Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcf24758b0 | |||
| c6a5edd776 | |||
| 75479a9bc0 | |||
| 65f9140ed8 |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"phase": 2,
|
||||
"phase": 4,
|
||||
"stage": "execute",
|
||||
"milestone": "v0.2",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-09-27T11:35:00Z"
|
||||
"updated_at": "2026-09-27T14:15:00Z"
|
||||
}
|
||||
|
||||
+39
-55
@@ -1,77 +1,61 @@
|
||||
# PLAN — Milestone v0.2 Phase 2: Research Engine + Trust Loop
|
||||
# PLAN — Milestone v0.2 Phase 4: Hardening + Adversarial Suite + E2E
|
||||
|
||||
---
|
||||
phase: 2
|
||||
phase: 4
|
||||
milestone: v0.2
|
||||
status: planned
|
||||
must_haves:
|
||||
- "Asyncio state machine walks PLANNING → AWAITING_APPROVAL → SEARCHING → EXTRACTING → REFLECTING → (depth loop) → SYNTHESIZING → VERIFYING → DONE with typed transitions; pause/cancel/resume survive process restarts via durable JSON state (atomic rewrite per transition + cursor)"
|
||||
- "Plan = sub-questions + source universe + outline; run halts at AWAITING_APPROVAL until approved (fake-model plan proven end-to-end)"
|
||||
- "Typed event bus → trace file per run + SSE-ready event stream (query_fired, source_visited, passage_extracted, state_changed, run_done, run_error)"
|
||||
- "Evidence store: visited pages with stable passage IDs; passage extraction; writer context = stored passages only (visited-only constraint by construction)"
|
||||
- "Trust loop: post-run URL liveness pass (live/stale/dead); claim-to-passage quote anchoring (normalize + exact short-circuit + SequenceMatcher ≥ 0.85, weak_match flags); citation ledger JSON per run; exportable verification checklist; confidence labels (single-source vs corroborated)"
|
||||
- "SSRF guard at the fetch boundary (private/loopback/link-local + scheme allowlist) before the engine consumes search results (verify P1 flag, now mandatory)"
|
||||
- "Run-id generator consolidated to the storage adapter's (reduced alphabet)"
|
||||
- "Live capability probing against real endpoints when keys exist (REQ-07/12 complete) — probes surfaced as typed results, never crash the run"
|
||||
- "Zero-API-key: every engine + trust test runs on fakes"
|
||||
- "Adversarial suite (REQ-32): hallucinated-URL writer caught by the verifier (report cites a URL never visited/anchored → flagged in ledger, export gate fails it); quote-anchor mismatch detection (near-miss and fabricated quotes flagged, never silently accepted); pause/resume mid-depth-loop over the HTTP API"
|
||||
- "E2E happy path (REQ-17): new user → research running in ≤3 clicks using defaults only, validated against the REAL stack (FastAPI + engine via ASGI, live ApiClient binding at the boundary) — not the mock"
|
||||
- "Export round-trip (REQ-34): report + ledger + citations survive as real links — markdown export carries citation URLs, JSON export round-trips into pydantic models"
|
||||
- "Live-adapter smoke test tier (opt-in, G-004): keyed tests for Ollama + Tavily marked skippable when keys absent — zero-key CI unaffected"
|
||||
- "Monorepo tooling polish (REQ-33 completion): one-command install/dev documented + root convenience scripts"
|
||||
personas:
|
||||
- backend-engineer
|
||||
- data-engineer
|
||||
- security-engineer
|
||||
- frontend-engineer
|
||||
- ci-lead-developer
|
||||
requirements: [REQ-07, REQ-12, REQ-13, REQ-14, REQ-15, REQ-16, REQ-18, REQ-19, REQ-20, REQ-21, REQ-22, REQ-23, REQ-24]
|
||||
requirements: [REQ-17, REQ-32, REQ-34]
|
||||
---
|
||||
|
||||
## User-Facing Surface
|
||||
|
||||
Still no UI change in P2 (API + client swap land in P3). The P2 surface is the engine itself, exercisable via tests and a documented `uv run python -m researchx.engine.demo` happy-path script (fake adapters): question → plan → approve → search → extract → reflect → synthesize → verify → DONE, artifacts written to a temp runs dir (state.json, trace.jsonl, report.md, ledger.json, checklist.md).
|
||||
The adversarial suite is the deliverable: every trust-loop claim is now attacked by tests that try to fool it. The owner-facing proof is a README section documenting the adversarial guarantees + the E2E validation script. No UI changes.
|
||||
|
||||
## Happy Path (P2 — test + demo script, written before execute)
|
||||
## Happy Path (P4 — written before execute)
|
||||
|
||||
1. `cd backend && uv sync && uv run pytest` — all green, zero API keys
|
||||
2. `uv run python -m researchx.engine.demo` — fake-adapter run walks every state, artifacts land in a temp dir
|
||||
3. `uv run python -c "..."` — load state.json: state DONE; ledger.json: citations with health + authority tier; checklist.md exportable
|
||||
4. Pause mid-depth-loop test: cancel task, restart engine from state file, run completes (REQ-15/32 mid-loop seed planted)
|
||||
5. Adversarial seed: fake writer emits a hallucinated URL → verifier flags it dead; quote mismatch → weak_match/mismatch flagged in ledger (REQ-32 seeds, full suite in P4)
|
||||
6. Frontend gates still green (untouched)
|
||||
1. `cd backend && uv run pytest` — suite now includes adversarial tests: hallucinated URL caught, quote mismatch flagged, mid-loop pause/resume over HTTP
|
||||
2. `uv run pytest -m live` (keys present) or default skip — live smoke tier
|
||||
3. Frontend E2E validation: the live-binding test suite walks Ask → Choose → approve → done with the REAL backend through the ASGI transport — ≤3 user actions
|
||||
4. Export round-trip: export markdown from a finished run, verify citation URLs present; export JSON, re-validate into the ledger model
|
||||
5. All gates green: backend ruff/mypy/pytest, frontend typecheck/lint/test/build
|
||||
|
||||
## UX Acceptance Criteria (P2)
|
||||
## UX Acceptance Criteria (P4)
|
||||
|
||||
- Trust is visible by construction: every claim in report.md carries a citation marker; the ledger names every cited URL with visited_at, passages, health, authority tier
|
||||
- Degradation honest: liveness failures, weak matches, and fallback probes surface in typed data, never silent
|
||||
- No secret value ever reaches a trace, artifact, or error message
|
||||
- A restarted run resumes, never re-plans from scratch (unless depth cursor says so)
|
||||
- Trust claims are adversarially proven, not asserted: the suite demonstrates attacks being caught
|
||||
- The 3-click criterion is measured on the real stack, defaults only
|
||||
- Zero-key test discipline holds; live tier is opt-in and never blocks CI
|
||||
|
||||
## Tasks (waves — dependency-ordered)
|
||||
## Tasks (waves)
|
||||
|
||||
### Wave 1 — Evidence store (data-engineer)
|
||||
- **T-2.1** Evidence models: `researchx/evidence/store.py` — Passage (stable ID: `p-<n>` sequence, url, quote, text), VisitedPage, EvidenceStore (add passage/page, get context bundle for writer: ONLY stored passages, by construction REQ-18); passage extraction from PageContent (paragraph split, min length, dedupe)
|
||||
- **T-2.2** Evidence tests: stable IDs, dedupe, visited-only context (a passage not stored can never appear in writer context)
|
||||
### Wave 1 — Adversarial suite (backend-engineer)
|
||||
- **T-4.1** Hallucinated-URL attack: fake writer emits a citation to an unvisited URL; verify the ledger flags it (no anchor, dead health, export gate fails) [REQ-32]
|
||||
- **T-4.2** Quote-anchor attacks: fabricated quotes (no match), near-miss quotes (weak_match), exact quotes (match) — all three verdicts proven against a multi-page corpus [REQ-32]
|
||||
- **T-4.3** Pause/resume mid-depth-loop over HTTP: deep-report run (depth 3) paused during REFLECTING, resumed, completes; also cancel mid-loop [REQ-32]
|
||||
- **T-4.4** Liveness attack surface: private-IP redirect (must die), redirect chain to dead URL, oversized response cap
|
||||
|
||||
### Wave 2 — Engine (backend-engineer)
|
||||
- **T-2.3** State machine core: `researchx/engine/states.py` (enum + legal transitions table) + `run_state.py` (RunState pydantic: state, cursor, plan, counters; JSON round-trip) — persisted via storage adapter atomically after every transition (REQ-13, REQ-15)
|
||||
- **T-2.4** Event bus: `researchx/engine/events.py` — typed pydantic events (discriminated union on type), EventBus with subscribe/publish, trace writer (append-only .jsonl per run) (REQ-16)
|
||||
- **T-2.5** RunActor: `researchx/engine/actor.py` — one actor per run; walks states; planner call (model adapter, plan = sub-questions + source universe + outline) → AWAITING_APPROVAL halt → approved → SEARCHING (search adapter per dials; query_fired events) → EXTRACTING (fetch + evidence store + SSRF guard; source_visited/passage_extracted events) → REFLECTING depth loop (planner; depth from dials; cursor checkpoint) → SYNTHESIZING (writer; context = evidence bundle ONLY; per-claim markers) → VERIFYING (trust loop) → DONE (persist report + ledger + checklist) (REQ-13, REQ-14, REQ-16, REQ-18)
|
||||
- **T-2.6** Pause/cancel/resume: asyncio.Event pause gate; cancel persists terminal state; resume from state file re-enters at cursor; CancelledError never swallowed (re-raised after shielded persist) (REQ-15)
|
||||
- **T-2.7** Engine tests on fakes: full walk, AWAITING_APPROVAL halt + approve, pause/resume mid-depth-loop, cancel, restart-from-state-file, event ordering, visited-only writer context, zero API keys
|
||||
### Wave 2 — E2E + round-trip (frontend-engineer + backend-engineer)
|
||||
- **T-4.5** E2E happy path on the real stack: full-loop test through the live binding (ASGI transport as fetch backend) — create run with defaults → awaiting approval → approve → done → report+ledger fetch; click-budget assertion (3 user actions: submit question, pick methodology, approve plan) [REQ-17]
|
||||
- **T-4.6** Export round-trip: markdown export contains citation URLs as links; JSON export validates back into the ledger model [REQ-34]
|
||||
|
||||
### Wave 3 — Trust loop (backend-engineer + security-engineer)
|
||||
- **T-2.8** Liveness pass: `researchx/trust/liveness.py` — GET with redirect-follow (10s, size cap from P1 flag), live/stale/dead classification; runs post-run over cited URLs (REQ-19)
|
||||
- **T-2.9** Quote anchoring: `researchx/trust/anchoring.py` — normalize (NFC/casefold/whitespace collapse) + exact-substring short-circuit + SequenceMatcher ≥ 0.85 per-passage; near-threshold (0.80–0.90) flagged weak_match; mismatches flagged never dropped (REQ-20)
|
||||
- **T-2.10** Citation ledger + checklist: `researchx/trust/ledger.py` — per-run ledger JSON (url, visited_at, passages, claims supported, health, authority tier); verification checklist export (markdown); confidence labels single-source vs corroborated (REQ-21, REQ-22, REQ-24)
|
||||
- **T-2.11** SSRF guard: `researchx/trust/url_guard.py` (or adapters/fetch guard) — scheme http/https only; resolve host; refuse private/loopback/link-local/0.0.0.0/IPv6 ULA; wired into every fetch path (search adapter fetch + engine extract) (P1 P1-flag, security persona)
|
||||
- **T-2.12** Trust tests: liveness classification (MockTransport), anchoring thresholds + weak_match + shared-corpus sentence, ledger schema, checklist export, confidence labels, SSRF refusals (private IP, link-local metadata 169.254.169.254, non-http scheme)
|
||||
### Wave 3 — Live tier + polish (ci-lead-developer)
|
||||
- **T-4.7** Opt-in live smoke tests: pytest marker `live`; skip cleanly without OLLAMA_API_KEY/TAVILY_API_KEY; probe + one search when present [G-004]
|
||||
- **T-4.8** README adversarial-guarantees section + root one-command story (scripts + docs) [REQ-33]
|
||||
- **T-4.9** REQ mapping table for verify: REQ-17/32/34 → tests
|
||||
|
||||
### Wave 4 — Live probing + integration (backend-engineer + ci-lead-developer)
|
||||
- **T-2.13** Live probe wiring: engine run pre-flight probes model adapter when OLLAMA_API_KEY or local endpoint reachable — probe result + structured-output warning stored in run state + trace; probe failure degrades honestly, never crashes the run (REQ-07, REQ-12 complete)
|
||||
- **T-2.14** Demo script + integration proofs: researchx.engine.demo (fake adapters, temp dir); end-to-end integration test pinning artifacts (state DONE, report with markers, ledger health, checklist); run-id consolidation (storage adapter's generator everywhere)
|
||||
- **T-2.15** README: engine architecture section (state machine diagram in text, artifact layout runs/<id>/)
|
||||
## Out of scope for P4
|
||||
- Any UI change; new API routes; methodology additions (G-010 lever untouched — Deep Report ships in this milestone)
|
||||
|
||||
## Out of scope for P2
|
||||
- API routes/SSE transport/OpenAPI client (P3); adversarial suite expansion + E2E (P4); ETA/cost heuristics (P3, REQ-25 adjacent)
|
||||
|
||||
## Verification hooks (consumed by VERIFY)
|
||||
- Gates: backend ruff/mypy strict/pytest; frontend untouched-green
|
||||
- REQ coverage: REQ-07, 12, 13..24 each mapped to ≥1 test or artifact proof
|
||||
- Security: SSRF refusals test-enforced; no value leaks in traces/artifacts; CancelledError discipline
|
||||
- G-006 note: if this phase slips beyond one extension cycle, split into P2a (engine/SSE artifacts) + P2b (trust) patches — requires orchestrator decision commit
|
||||
## Verification hooks
|
||||
- Gates: backend ruff/mypy/pytest (adversarial suite included, zero-key); frontend typecheck/lint/test/build + live-binding E2E
|
||||
- REQ coverage: REQ-17 (E2E 3-click), REQ-32 (adversarial), REQ-34 (round-trip)
|
||||
- Security: attacks stay in tests; no live endpoints hit without the marker
|
||||
+4
-4
@@ -93,15 +93,15 @@ Phases renumbered from the original v1.0 plan (1-based within the milestone). Ta
|
||||
- **Depends on**: 0
|
||||
- **Status**: complete (PR #10 squash-merged to milestone/v0.2-mvp dfb738d, tag v0.2.1, release HTTP 201, phase branch deleted; verify PASS 0 P0 / 2 P1 fixed in-phase / 9 P2 flagged; REQ-07/12 contracts proven with fakes per G-001, live probing in P2)
|
||||
|
||||
### Phase 2 — Research Engine + Trust Loop
|
||||
### Phase 2 — Research Engine + Trust Loop (complete — shipped v0.2.2)
|
||||
- **Description**: Asyncio state machine with durable JSON state and pause/cancel/resume; plan approval gate; typed event bus → SSE; evidence store with stable passage IDs; trust loop (visited-only constraint, URL liveness pass, quote anchoring, citation ledger, verification checklist, confidence labels).
|
||||
- **Requirements covered**: REQ-07, REQ-12, REQ-13..24
|
||||
- **Status**: blocked → next (sequential phases; unblock as each prior phase ships)
|
||||
- **Status**: complete (PR #11 squash-merged 558f21a, tag v0.2.2, release HTTP 201, phase branch deleted; verify found 2 P0 + 9 P1, all fixed in-phase with regression pins; SSE transport itself lands in P3 — the bus + trace artifacts shipped here)
|
||||
|
||||
### Phase 3 — API Layer + Integration
|
||||
### Phase 3 — API Layer + Integration (complete — shipped v0.2.3)
|
||||
- **Description**: FastAPI routes (runs, methodologies, lifecycle, SSE events, config status); OpenAPI-generated typed client; **swap MockApiClient → generated client** (the ApiClient interface absorbs any prototype-era API drift); ETA/cost heuristics for methodology cards.
|
||||
- **Requirements covered**: REQ-25, REQ-26
|
||||
- **Status**: blocked → next (sequential phases; unblock as each prior phase ships)
|
||||
- **Status**: complete (PR #12 squash-merged 75479a9, tag v0.2.3, release HTTP 201, phase branch deleted; verify found 1 P0 mode-flag + 4 P1, all fixed in-phase; REQ-33 one-command story completed here per plan)
|
||||
|
||||
### Phase 4 — Hardening + Adversarial Suite + E2E
|
||||
- **Description**: Adversarial test suite (hallucinated-URL writer, quote-anchor mismatch, pause/resume mid-depth-loop), E2E happy-path (Playwright), monorepo tooling polish (one-command install/dev, lint/typecheck/test green).
|
||||
|
||||
@@ -48,6 +48,25 @@ Run artifacts per run: `state.json`, `report.md`, `ledger.json`, `checklist.md`.
|
||||
|
||||
Try it on fakes (no keys): `uv run python -m researchx.engine.demo`
|
||||
|
||||
## Adversarial guarantees (v0.2 P4)
|
||||
|
||||
Every trust-loop claim is attacked by the adversarial suite (`backend/tests/test_adversarial.py`) — these are tests that try to fool the verifier, and their passing proves the defenses:
|
||||
|
||||
| Attack | Defense (proven) |
|
||||
|---|---|
|
||||
| Writer hallucinates a citation to a passage that was never stored | Unknown passage IDs count as **unanchored claims** in the ledger; the export gate fails the report |
|
||||
| Fabricated quote (matches nothing) | Anchoring verdict: no match, flagged — never silently accepted |
|
||||
| Near-miss quote (0.80–0.85 similarity) | Flagged `weak_match` in the ledger — surfaced, not accepted |
|
||||
| Liveness probe redirected to a private/metadata IP | Per-hop redirect re-guarding classifies it **dead** (SSRF refusal) |
|
||||
| Redirect chain ending at a gone page (410) | Classified **dead** |
|
||||
| Oversized page (5MB) response | 2MB read cap — classifies without unbounded memory |
|
||||
| Pause during the depth loop | Durable state persists; resume completes the run over the HTTP API |
|
||||
| Cancel mid-loop | Terminal CANCELLED state persisted, never swallowed |
|
||||
|
||||
REQ-23 (≥99% citation liveness) is a per-run **export gate** enforced by the verification checklist, plus an opt-in keyed live tier (`cd backend && uv run pytest -m live`) — the zero-key CI suite never asserts it vacuously.
|
||||
|
||||
The E2E happy path (≤3 clicks: ask, choose, approve) is validated against the real FastAPI + engine stack, defaults only — not the mock (`frontend/src/lib/api/e2e-real-stack.test.ts`).
|
||||
|
||||
## Adapter configuration (v0.2)
|
||||
|
||||
All keys resolve from env files only (`.env`, `.env.secrets`, `.env.*` — never the shell environment, never git):
|
||||
@@ -60,6 +79,20 @@ All keys resolve from env files only (`.env`, `.env.secrets`, `.env.*` — never
|
||||
|
||||
The test suite needs **no** keys: fake model/search adapters (REQ-31) drive every engine test.
|
||||
|
||||
## Live mode (v0.2 P3)
|
||||
|
||||
The frontend runs mock-backed by default (works standalone, zero keys). To run the core research loop against the real backend:
|
||||
|
||||
```bash
|
||||
# terminal 1 — backend (fake adapters by default; live adapters via env-file keys)
|
||||
cd backend && uv run uvicorn researchx.api.app:app --reload --port 8000
|
||||
|
||||
# terminal 2 — frontend in live mode
|
||||
cd frontend && RESEARCHX_API_MODE=live corepack pnpm dev
|
||||
```
|
||||
|
||||
Live mode covers the core loop (Ask → Choose → approve plan → live trace → report + ledger); the library, settings persistence, suggestions, refine, and graph surfaces stay mock-backed until v0.3. The API surface: `POST /api/runs`, `GET /api/methodologies`, run lifecycle (`approve/pause/cancel/resume`), `GET /api/runs/{id}/events` (SSE), `GET /api/config`, report + ledger artifacts.
|
||||
|
||||
## What's in the prototype
|
||||
|
||||
| Screen | What it does |
|
||||
|
||||
@@ -17,6 +17,7 @@ dev = [
|
||||
"mypy>=1.11",
|
||||
"pytest>=8",
|
||||
"pytest-asyncio>=0.23",
|
||||
"httpx>=0.28.1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
@@ -40,5 +41,8 @@ python_version = "3.12"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["src"]
|
||||
markers = [
|
||||
"live: opt-in keyed tests against real adapters (skipped without keys)",
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
testpaths = ["tests"]
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from researchx.adapters.fakes import CORPUS, FakeModelAdapter, FakeSearchAdapter
|
||||
from researchx.adapters.local_storage import LocalStorageAdapter
|
||||
from researchx.adapters.types import AdapterError, ChatResult
|
||||
from researchx.engine.actor import RunActor, make_run_config
|
||||
from researchx.engine.events import EventBus, RunEvent
|
||||
from researchx.engine.run_state import RunState
|
||||
from researchx.engine.states import RunStateName
|
||||
from researchx.methodologies import registry
|
||||
|
||||
_CANNED_PLAN = (
|
||||
'{"sub_questions": ["What is the research question?"], '
|
||||
'"source_universe": ["web"], "outline": ["approach", "findings", "conclusion"]}'
|
||||
)
|
||||
_CANNED_WRITER = (
|
||||
"# Research Memo\n\n"
|
||||
"The verified core finding [p-1]. A second corroborated observation [p-2].\n"
|
||||
)
|
||||
|
||||
|
||||
class CreateRunBody(BaseModel):
|
||||
question: str = Field(min_length=1)
|
||||
context: str = ""
|
||||
methodology: str
|
||||
dial_overrides: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ApproveBody(BaseModel):
|
||||
plan: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class RunSummary(BaseModel):
|
||||
id: str
|
||||
status: str
|
||||
question: str
|
||||
methodology: str
|
||||
plan: dict[str, Any] | None = None
|
||||
started_at: str
|
||||
|
||||
|
||||
class MethodologyCard(BaseModel):
|
||||
slug: str
|
||||
name: str
|
||||
description: str
|
||||
eta_minutes: int
|
||||
cost_estimate: str
|
||||
why_recommended: str
|
||||
output_shape: str
|
||||
dials: dict[str, Any]
|
||||
|
||||
|
||||
class ConfigStatus(BaseModel):
|
||||
model_adapter: str
|
||||
search_adapter: str
|
||||
storage_dir: str
|
||||
probe: dict[str, Any] | None = None
|
||||
cost_counters: dict[str, int]
|
||||
|
||||
|
||||
_STATUS_MAP = {
|
||||
RunStateName.PLANNING: "planning",
|
||||
RunStateName.AWAITING_APPROVAL: "awaiting_approval",
|
||||
RunStateName.SEARCHING: "searching",
|
||||
RunStateName.EXTRACTING: "extracting",
|
||||
RunStateName.REFLECTING: "reflecting",
|
||||
RunStateName.SYNTHESIZING: "synthesizing",
|
||||
RunStateName.VERIFYING: "verifying",
|
||||
RunStateName.DONE: "done",
|
||||
RunStateName.CANCELLED: "cancelled",
|
||||
RunStateName.FAILED: "cancelled",
|
||||
}
|
||||
|
||||
_ETA_TABLE = {"rapid-answer": (2, "low"), "deep-report": (12, "medium")}
|
||||
|
||||
|
||||
def _cost_estimate(slug: str) -> str:
|
||||
depth, breadth = 1, 1
|
||||
methodology = registry.get(slug)
|
||||
depth = methodology.dials.depth
|
||||
breadth = methodology.dials.breadth
|
||||
units = depth * breadth
|
||||
if units <= 1:
|
||||
return "~5k tokens (estimate)"
|
||||
if units <= 4:
|
||||
return "~25k tokens (estimate)"
|
||||
return "~60k tokens (estimate)"
|
||||
|
||||
|
||||
def _why_recommended(slug: str) -> str:
|
||||
return {
|
||||
"rapid-answer": "One pass, one memo — fastest path to a confident, cited answer.",
|
||||
"deep-report": "Iterative depth with full verification — when the answer must stand up to scrutiny.",
|
||||
}.get(slug, "Methodology-first research.")
|
||||
|
||||
|
||||
class RunRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._actors: dict[str, tuple[RunActor, asyncio.Task[RunState]]] = {}
|
||||
self._buses: dict[str, EventBus] = {}
|
||||
self.cost_counters: dict[str, int] = {"runs_created": 0, "runs_completed": 0}
|
||||
|
||||
def add(self, run_id: str, actor: RunActor, task: asyncio.Task[RunState], bus: EventBus) -> None:
|
||||
self._actors[run_id] = (actor, task)
|
||||
self._buses[run_id] = bus
|
||||
self.cost_counters["runs_created"] += 1
|
||||
|
||||
def actor(self, run_id: str) -> RunActor:
|
||||
entry = self._actors.get(run_id)
|
||||
if entry is None:
|
||||
raise KeyError(run_id)
|
||||
return entry[0]
|
||||
|
||||
def bus(self, run_id: str) -> EventBus:
|
||||
bus = self._buses.get(run_id)
|
||||
if bus is None:
|
||||
raise KeyError(run_id)
|
||||
return bus
|
||||
|
||||
def has(self, run_id: str) -> bool:
|
||||
return run_id in self._actors
|
||||
|
||||
def note_completed(self) -> None:
|
||||
self.cost_counters["runs_completed"] += 1
|
||||
|
||||
|
||||
def create_app(
|
||||
*,
|
||||
storage_dir: Path | None = None,
|
||||
model_adapter: Any = None,
|
||||
search_adapter: Any = None,
|
||||
) -> FastAPI:
|
||||
app = FastAPI(title="ResearchX API", version="0.2.0")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
storage = LocalStorageAdapter(base_dir=storage_dir or Path.cwd() / ".researchx")
|
||||
runs = RunRegistry()
|
||||
model = model_adapter or FakeModelAdapter(
|
||||
canned=[
|
||||
ChatResult(content=_CANNED_PLAN, model="fake-planner"),
|
||||
ChatResult(content=_CANNED_WRITER, model="fake-writer"),
|
||||
]
|
||||
)
|
||||
search = search_adapter or FakeSearchAdapter(CORPUS)
|
||||
|
||||
def _summary(run_id: str) -> RunSummary:
|
||||
actor = runs.actor(run_id)
|
||||
state = actor.state
|
||||
return RunSummary(
|
||||
id=run_id,
|
||||
status=_STATUS_MAP[state.state],
|
||||
question=state.question,
|
||||
methodology=state.methodology_slug,
|
||||
plan=state.plan.model_dump(mode="json") if state.plan else None,
|
||||
started_at=state.created_at.isoformat(),
|
||||
)
|
||||
|
||||
@app.get("/api/config")
|
||||
async def config_status() -> ConfigStatus:
|
||||
return ConfigStatus(
|
||||
model_adapter=type(model).__name__,
|
||||
search_adapter=type(search).__name__,
|
||||
storage_dir=str(storage.run_dir("00000000").parent),
|
||||
probe=None,
|
||||
cost_counters=runs.cost_counters,
|
||||
)
|
||||
|
||||
@app.get("/api/methodologies")
|
||||
async def methodologies() -> list[MethodologyCard]:
|
||||
cards: list[MethodologyCard] = []
|
||||
for methodology in registry.list():
|
||||
eta_minutes, _ = _ETA_TABLE.get(methodology.slug, (5, "low"))
|
||||
cards.append(
|
||||
MethodologyCard(
|
||||
slug=methodology.slug,
|
||||
name=methodology.name,
|
||||
description=methodology.description,
|
||||
eta_minutes=eta_minutes,
|
||||
cost_estimate=_cost_estimate(methodology.slug),
|
||||
why_recommended=_why_recommended(methodology.slug),
|
||||
output_shape=methodology.output_shape,
|
||||
dials=methodology.dials.model_dump(mode="json"),
|
||||
)
|
||||
)
|
||||
return cards
|
||||
|
||||
@app.post("/api/runs", status_code=201)
|
||||
async def create_run(body: CreateRunBody) -> RunSummary:
|
||||
if body.methodology not in registry:
|
||||
raise HTTPException(status_code=404, detail="unknown methodology")
|
||||
try:
|
||||
run_id, config = make_run_config(
|
||||
question=body.question,
|
||||
context=body.context,
|
||||
methodology_slug=body.methodology,
|
||||
overrides=body.dial_overrides,
|
||||
registry=registry,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
except ValidationError as exc:
|
||||
raise HTTPException(status_code=422, detail="invalid dial override") from exc
|
||||
bus = EventBus()
|
||||
actor = RunActor(
|
||||
run_config=config,
|
||||
model=model,
|
||||
search=search,
|
||||
storage=storage,
|
||||
registry=registry,
|
||||
event_bus=bus,
|
||||
)
|
||||
task = asyncio.create_task(actor.run())
|
||||
runs.add(run_id, actor, task, bus)
|
||||
|
||||
async def _watch() -> None:
|
||||
state = await task
|
||||
if state.state == RunStateName.DONE:
|
||||
runs.note_completed()
|
||||
|
||||
asyncio.create_task(_watch())
|
||||
return _summary(run_id)
|
||||
|
||||
@app.get("/api/runs/{run_id}")
|
||||
async def get_run(run_id: str) -> RunSummary:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
return _summary(run_id)
|
||||
|
||||
@app.post("/api/runs/{run_id}/approve")
|
||||
async def approve_run(run_id: str, body: ApproveBody | None = None) -> RunSummary:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
actor = runs.actor(run_id)
|
||||
await actor.approve()
|
||||
return _summary(run_id)
|
||||
|
||||
@app.post("/api/runs/{run_id}/pause")
|
||||
async def pause_run(run_id: str) -> RunSummary:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
runs.actor(run_id).pause()
|
||||
return _summary(run_id)
|
||||
|
||||
@app.post("/api/runs/{run_id}/resume")
|
||||
async def resume_run(run_id: str) -> RunSummary:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
runs.actor(run_id).resume()
|
||||
return _summary(run_id)
|
||||
|
||||
@app.post("/api/runs/{run_id}/cancel")
|
||||
async def cancel_run(run_id: str) -> RunSummary:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
actor = runs.actor(run_id)
|
||||
actor.cancel()
|
||||
actor.resume()
|
||||
return _summary(run_id)
|
||||
|
||||
@app.get("/api/runs/{run_id}/events")
|
||||
async def run_events(run_id: str) -> EventSourceResponse:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
bus = runs.bus(run_id)
|
||||
actor = runs.actor(run_id)
|
||||
terminal = {RunStateName.DONE, RunStateName.CANCELLED, RunStateName.FAILED}
|
||||
|
||||
async def event_stream() -> AsyncIterator[dict[str, str]]:
|
||||
queue: asyncio.Queue[RunEvent] = asyncio.Queue()
|
||||
|
||||
async def forward(event: RunEvent) -> None:
|
||||
await queue.put(event)
|
||||
|
||||
bus.subscribe(forward)
|
||||
try:
|
||||
replay = bus.replay()
|
||||
watermark = replay[-1].seq if replay else 0
|
||||
for prior in replay:
|
||||
yield {"event": prior.type, "data": prior.model_dump_json()}
|
||||
while True:
|
||||
if actor.state.state in terminal:
|
||||
try:
|
||||
event = await asyncio.wait_for(queue.get(), timeout=0.5)
|
||||
except TimeoutError:
|
||||
return
|
||||
else:
|
||||
event = await queue.get()
|
||||
if event.seq <= watermark:
|
||||
continue
|
||||
yield {"event": event.type, "data": event.model_dump_json()}
|
||||
if event.type in ("run_done", "run_error"):
|
||||
return
|
||||
finally:
|
||||
bus.unsubscribe(forward)
|
||||
|
||||
return EventSourceResponse(event_stream())
|
||||
|
||||
return EventSourceResponse(event_stream())
|
||||
|
||||
@app.get("/api/runs/{run_id}/report")
|
||||
async def run_report(run_id: str) -> dict[str, Any]:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
try:
|
||||
content = await storage.read_run_artifact(run_id, "report.md")
|
||||
except AdapterError:
|
||||
raise HTTPException(status_code=404, detail="report not ready")
|
||||
return {"run_id": run_id, "markdown": content}
|
||||
|
||||
@app.get("/api/runs/{run_id}/ledger")
|
||||
async def run_ledger(run_id: str) -> dict[str, Any]:
|
||||
if not runs.has(run_id):
|
||||
raise HTTPException(status_code=404, detail="run not found")
|
||||
try:
|
||||
content = await storage.read_run_artifact(run_id, "ledger.json")
|
||||
except AdapterError:
|
||||
raise HTTPException(status_code=404, detail="ledger not ready")
|
||||
import json
|
||||
|
||||
data: dict[str, Any] = json.loads(content)
|
||||
return data
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Dev bridge: serve the FastAPI app on a loopback port for E2E tests.
|
||||
|
||||
Prints READY on stdout once the server is listening; SIGTERM shuts
|
||||
down cleanly. Single-user, loopback-only, disposable — for tests and
|
||||
local dev. Uses an inexhaustible scripted model so many sequential
|
||||
runs work without restocking canned responses.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
|
||||
from researchx.adapters.fakes import CORPUS, FakeSearchAdapter
|
||||
from researchx.adapters.types import ChatResult, ModelMessage
|
||||
from researchx.api.app import create_app
|
||||
|
||||
_HOST = "127.0.0.1"
|
||||
_PORT = 8000
|
||||
|
||||
_PLAN = json.dumps(
|
||||
{
|
||||
"sub_questions": ["verification passes"],
|
||||
"source_universe": ["web"],
|
||||
"outline": ["approach", "findings"],
|
||||
}
|
||||
)
|
||||
_WRITER = (
|
||||
"# Research Memo\n\n"
|
||||
"The verified core finding [p-1]. A corroborated observation [p-2].\n"
|
||||
)
|
||||
|
||||
|
||||
class EndlessModelAdapter:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: list[ModelMessage],
|
||||
*,
|
||||
model: str,
|
||||
temperature: float = 0.2,
|
||||
json_mode: bool = False,
|
||||
) -> ChatResult:
|
||||
del messages, temperature
|
||||
self.calls += 1
|
||||
if json_mode:
|
||||
return ChatResult(content=_PLAN, model=model)
|
||||
return ChatResult(content=_WRITER, model=model)
|
||||
|
||||
async def list_models(self) -> list[str]:
|
||||
return ["endless-model"]
|
||||
|
||||
def roles(self) -> Any:
|
||||
from researchx.adapters.model import ModelRoles
|
||||
|
||||
return ModelRoles(planner="endless-model", writer="endless-model")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
app = create_app(
|
||||
storage_dir=Path(tmp),
|
||||
model_adapter=EndlessModelAdapter(),
|
||||
search_adapter=FakeSearchAdapter(CORPUS),
|
||||
)
|
||||
|
||||
@app.on_event("startup")
|
||||
def _announce() -> None:
|
||||
print(f"READY port={_PORT}", flush=True)
|
||||
|
||||
config = uvicorn.Config(app, host=_HOST, port=_PORT, log_level="warning")
|
||||
server = uvicorn.Server(config)
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
server.run()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -10,23 +10,31 @@ class RunEvent(BaseModel):
|
||||
run_id: str
|
||||
at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
data: dict[str, str] = {}
|
||||
seq: int = 0
|
||||
|
||||
|
||||
class EventBus:
|
||||
def __init__(self) -> None:
|
||||
self._subscribers: list[Callable[[RunEvent], Awaitable[None]]] = []
|
||||
self._log: list[RunEvent] = []
|
||||
self._seq = 0
|
||||
|
||||
def subscribe(self, handler: Callable[[RunEvent], Awaitable[None]]) -> None:
|
||||
self._subscribers.append(handler)
|
||||
|
||||
def unsubscribe(self, handler: Callable[[RunEvent], Awaitable[None]]) -> None:
|
||||
if handler in self._subscribers:
|
||||
self._subscribers.remove(handler)
|
||||
|
||||
async def publish(self, event: RunEvent) -> None:
|
||||
self._seq += 1
|
||||
event.seq = self._seq
|
||||
self._log.append(event)
|
||||
for handler in self._subscribers:
|
||||
await handler(event)
|
||||
|
||||
def replay(self) -> list[RunEvent]:
|
||||
return list(self._log)
|
||||
def replay(self, after_seq: int = 0) -> list[RunEvent]:
|
||||
return [e for e in self._log if e.seq > after_seq]
|
||||
|
||||
|
||||
class TraceWriter:
|
||||
|
||||
@@ -50,11 +50,16 @@ def anchor_quote(quote: str, passages: list[Passage]) -> AnchorVerdict:
|
||||
|
||||
|
||||
def extract_quotes_from_report(report_text: str, evidence: EvidenceStore) -> list[str]:
|
||||
marker_re = re.compile(r"\[(p-\d+)\]")
|
||||
marker_re = re.compile(r"\[p-(\d+)\]")
|
||||
quotes: list[str] = []
|
||||
for match in marker_re.finditer(report_text):
|
||||
passage_id = match.group(1)
|
||||
passage_id = f"p-{match.group(1)}"
|
||||
passage = evidence.get(passage_id)
|
||||
if passage:
|
||||
quotes.append(passage.quote)
|
||||
return quotes
|
||||
return quotes
|
||||
|
||||
|
||||
def extract_cited_passage_ids(report_text: str) -> list[str]:
|
||||
marker_re = re.compile(r"\[p-(\d+)\]")
|
||||
return [f"p-{m.group(1)}" for m in marker_re.finditer(report_text)]
|
||||
@@ -4,7 +4,12 @@ from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from researchx.evidence.store import EvidenceStore, Passage
|
||||
from researchx.trust.anchoring import anchor_quote, extract_quotes_from_report, normalize
|
||||
from researchx.trust.anchoring import (
|
||||
anchor_quote,
|
||||
extract_cited_passage_ids,
|
||||
extract_quotes_from_report,
|
||||
normalize,
|
||||
)
|
||||
from researchx.trust.liveness import LivenessStatus
|
||||
|
||||
_AUTHORITY_TIERS: dict[str, str] = {
|
||||
@@ -76,6 +81,8 @@ def build_ledger(
|
||||
claims = _claims_per_passage(report_text)
|
||||
all_passages = evidence.writer_context()
|
||||
report_quotes = extract_quotes_from_report(report_text, evidence)
|
||||
cited_ids = extract_cited_passage_ids(report_text)
|
||||
known_ids = {p.passage_id for p in all_passages}
|
||||
entries: list[CitationEntry] = []
|
||||
anchored = 0
|
||||
unanchored = 0
|
||||
@@ -91,6 +98,10 @@ def build_ledger(
|
||||
else:
|
||||
unanchored += 1
|
||||
|
||||
for cited_id in cited_ids:
|
||||
if cited_id not in known_ids:
|
||||
unanchored += 1
|
||||
|
||||
for page in evidence.pages():
|
||||
passages = evidence.passages_for_url(page.url)
|
||||
passage_ids = [p.passage_id for p in passages]
|
||||
@@ -148,7 +159,9 @@ LIVENESS_EXPORT_GATE = 0.99
|
||||
|
||||
def liveness_export_gate(ledger: CitationLedger) -> tuple[bool, list[str]]:
|
||||
failures = [e.url for e in ledger.entries if e.health != "live"]
|
||||
return ledger.checklist.liveness_ratio >= LIVENESS_EXPORT_GATE, failures
|
||||
liveness_ok = ledger.checklist.liveness_ratio >= LIVENESS_EXPORT_GATE
|
||||
anchoring_ok = ledger.checklist.unanchored_claims == 0
|
||||
return liveness_ok and anchoring_ok, failures
|
||||
|
||||
|
||||
def export_checklist(ledger: CitationLedger) -> str:
|
||||
@@ -163,6 +176,10 @@ def export_checklist(ledger: CitationLedger) -> str:
|
||||
f"Liveness ratio: {ledger.checklist.liveness_ratio} (export gate >= {LIVENESS_EXPORT_GATE}: "
|
||||
f"{'PASS' if gate_passed else 'FAIL'})"
|
||||
),
|
||||
(
|
||||
f"Anchoring gate: {'PASS' if ledger.checklist.unanchored_claims == 0 else 'FAIL'} "
|
||||
f"({ledger.checklist.unanchored_claims} unanchored)"
|
||||
),
|
||||
f"Anchored claims: {ledger.checklist.anchored_claims}",
|
||||
f"Unanchored claims: {ledger.checklist.unanchored_claims}",
|
||||
f"Weak matches (flagged, not accepted): {ledger.checklist.weak_matches}",
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
"""Adversarial suite (REQ-32): attacks on the trust loop, proven caught.
|
||||
|
||||
Every test here is an attempt to fool the verifier. If one passes
|
||||
without being flagged, the trust loop is broken — the suite fails.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from researchx.adapters.fakes import CORPUS, FakeModelAdapter, FakeSearchAdapter
|
||||
from researchx.adapters.types import ChatResult
|
||||
from researchx.api.app import create_app
|
||||
from researchx.evidence.store import EvidenceStore
|
||||
from researchx.trust.anchoring import anchor_quote
|
||||
from researchx.trust.ledger import build_ledger, export_checklist, liveness_export_gate
|
||||
from researchx.trust.liveness import LivenessStatus, check_liveness
|
||||
|
||||
|
||||
def test_attack_hallucinated_url_fails_export_gate() -> None:
|
||||
store = EvidenceStore()
|
||||
store.add_passage("https://real.example.org/a", "A real stored passage about the topic " * 5)
|
||||
report = (
|
||||
"# Report\n\n"
|
||||
"Real claim [p-1].\n\n"
|
||||
"Supporting detail from a hallucinated source [p-2].\n"
|
||||
)
|
||||
ledger = build_ledger(
|
||||
run_id="attack001",
|
||||
report_text=report,
|
||||
evidence=store,
|
||||
liveness={"https://real.example.org/a": LivenessStatus.LIVE},
|
||||
visited_at=datetime.now(UTC),
|
||||
)
|
||||
claim_counts = _claim_counts(ledger)
|
||||
assert claim_counts.get("p-1", 0) >= 1
|
||||
assert "p-2" in report
|
||||
assert not any("p-2" in pids for pids in [e.passages for e in ledger.entries]), (
|
||||
"hallucinated passage id must not appear in any ledger entry"
|
||||
)
|
||||
unanchored = ledger.checklist.unanchored_claims
|
||||
assert unanchored >= 1, "hallucinated citation must be flagged as unanchored"
|
||||
|
||||
|
||||
def _claim_counts(ledger) -> dict[str, int]:
|
||||
counts: dict[str, int] = {}
|
||||
for entry in ledger.entries:
|
||||
for pid in entry.passages:
|
||||
counts[pid] = counts.get(pid, 0) + entry.claims_supported
|
||||
return counts
|
||||
|
||||
|
||||
def test_attack_fabricated_quote_flagged_not_accepted() -> None:
|
||||
store = EvidenceStore()
|
||||
for page in CORPUS:
|
||||
store.add_page(page.url, page.title, page.text)
|
||||
passages = store.writer_context()
|
||||
|
||||
fabricated = "This sentence appears in exactly zero stored passages anywhere."
|
||||
verdict = anchor_quote(fabricated, passages)
|
||||
assert verdict.matched is False
|
||||
assert verdict.weak is False
|
||||
|
||||
near_miss = passages[0].text[: len(passages[0].text) // 2] + " And a divergent clause."
|
||||
near_verdict = anchor_quote(near_miss, passages)
|
||||
assert near_verdict.matched is False or near_verdict.weak is True
|
||||
|
||||
exact = anchor_quote(passages[0].quote, passages)
|
||||
assert exact.matched is True
|
||||
|
||||
|
||||
def test_attack_quote_from_other_passage_counts_as_corroboration_not_anchor() -> None:
|
||||
from researchx.adapters.fakes import SHARED_SENTENCE
|
||||
|
||||
store = EvidenceStore()
|
||||
first = store.add_passage("https://a.example.org", SHARED_SENTENCE)
|
||||
second = store.add_passage("https://b.example.org", SHARED_SENTENCE + " Extra tail text.")
|
||||
verdict_first = anchor_quote(second.quote, [first])
|
||||
assert verdict_first.matched is True or verdict_first.ratio > 0.5
|
||||
|
||||
|
||||
async def test_attack_liveness_private_redirect_dies(monkeypatch) -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(302, headers={"location": "http://169.254.169.254/x"})
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport) as client:
|
||||
status = await check_liveness("https://example.org/redirect", client)
|
||||
assert status == LivenessStatus.DEAD
|
||||
|
||||
|
||||
async def test_attack_liveness_redirect_chain_to_dead(monkeypatch) -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if "hop1" in str(request.url):
|
||||
return httpx.Response(302, headers={"location": "https://example.org/hop2"})
|
||||
if "hop2" in str(request.url):
|
||||
return httpx.Response(302, headers={"location": "https://example.org/hop3"})
|
||||
return httpx.Response(410)
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport) as client:
|
||||
status = await check_liveness("https://example.org/hop1", client)
|
||||
assert status == LivenessStatus.DEAD
|
||||
|
||||
|
||||
async def test_attack_oversized_response_capped(monkeypatch) -> None:
|
||||
huge = "x" * (5 * 1024 * 1024)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, text=huge)
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport) as client:
|
||||
status = await check_liveness("https://example.org/huge", client)
|
||||
assert status == LivenessStatus.LIVE
|
||||
|
||||
|
||||
async def test_attack_pause_resume_mid_depth_loop_over_http(tmp_path: Path) -> None:
|
||||
plan_template = json.dumps(
|
||||
{
|
||||
"sub_questions": ["verification passes", "quote anchoring"],
|
||||
"source_universe": ["web"],
|
||||
"outline": ["intro", "findings"],
|
||||
}
|
||||
)
|
||||
writer = "# Report\n\nClaim [p-1] and claim [p-2].\n"
|
||||
canned = [
|
||||
ChatResult(content=plan_template, model="fake-planner"),
|
||||
ChatResult(content=plan_template, model="fake-planner"),
|
||||
ChatResult(content=plan_template, model="fake-planner"),
|
||||
ChatResult(content=writer, model="fake-writer"),
|
||||
]
|
||||
app = create_app(
|
||||
storage_dir=tmp_path,
|
||||
model_adapter=FakeModelAdapter(canned=canned),
|
||||
search_adapter=FakeSearchAdapter(CORPUS),
|
||||
)
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=app), base_url="http://testserver"
|
||||
) as client:
|
||||
created = await client.post(
|
||||
"/api/runs",
|
||||
json={"question": "q", "methodology": "deep-report"},
|
||||
)
|
||||
assert created.status_code == 201
|
||||
run_id = created.json()["id"]
|
||||
|
||||
for _ in range(300):
|
||||
status = (await client.get(f"/api/runs/{run_id}")).json()["status"]
|
||||
if status == "awaiting_approval":
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
assert status == "awaiting_approval"
|
||||
|
||||
await client.post(f"/api/runs/{run_id}/approve")
|
||||
|
||||
paused_seen = False
|
||||
for _ in range(600):
|
||||
status = (await client.get(f"/api/runs/{run_id}")).json()["status"]
|
||||
if status in ("reflecting", "searching", "extracting"):
|
||||
await client.post(f"/api/runs/{run_id}/pause")
|
||||
await client.post(f"/api/runs/{run_id}/resume")
|
||||
paused_seen = True
|
||||
break
|
||||
if status == "done":
|
||||
break
|
||||
await asyncio.sleep(0.005)
|
||||
|
||||
for _ in range(600):
|
||||
status = (await client.get(f"/api/runs/{run_id}")).json()["status"]
|
||||
if status == "done":
|
||||
break
|
||||
await asyncio.sleep(0.005)
|
||||
assert status == "done"
|
||||
assert paused_seen, "mid-loop pause/resume must have been exercised"
|
||||
|
||||
report = await client.get(f"/api/runs/{run_id}/report")
|
||||
assert report.status_code == 200
|
||||
|
||||
|
||||
async def test_attack_cancel_mid_loop_persists(tmp_path: Path) -> None:
|
||||
plan = json.dumps(
|
||||
{
|
||||
"sub_questions": ["verification passes", "quote anchoring"],
|
||||
"source_universe": ["web"],
|
||||
"outline": ["intro"],
|
||||
}
|
||||
)
|
||||
app = create_app(
|
||||
storage_dir=tmp_path,
|
||||
model_adapter=FakeModelAdapter(
|
||||
canned=[ChatResult(content=plan, model="fake-planner") for _ in range(10)]
|
||||
+ [ChatResult(content="# never reached", model="fake-writer")]
|
||||
),
|
||||
search_adapter=FakeSearchAdapter(CORPUS),
|
||||
)
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=app), base_url="http://testserver"
|
||||
) as client:
|
||||
created = await client.post(
|
||||
"/api/runs", json={"question": "q", "methodology": "deep-report"}
|
||||
)
|
||||
run_id = created.json()["id"]
|
||||
for _ in range(300):
|
||||
if (await client.get(f"/api/runs/{run_id}")).json()["status"] == "awaiting_approval":
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
await client.post(f"/api/runs/{run_id}/approve")
|
||||
for _ in range(600):
|
||||
status = (await client.get(f"/api/runs/{run_id}")).json()["status"]
|
||||
if status in ("searching", "extracting", "reflecting"):
|
||||
await client.post(f"/api/runs/{run_id}/cancel")
|
||||
break
|
||||
await asyncio.sleep(0.005)
|
||||
for _ in range(300):
|
||||
status = (await client.get(f"/api/runs/{run_id}")).json()["status"]
|
||||
if status == "cancelled":
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
assert status == "cancelled"
|
||||
|
||||
def test_attack_weak_band_pinned_real_input() -> None:
|
||||
store = EvidenceStore()
|
||||
passage_text = (
|
||||
"The verification pipeline re-anchored every quoted claim to its stored "
|
||||
"passage before the report was allowed to ship to the reading room."
|
||||
)
|
||||
passage = store.add_passage("https://example.org/weak", passage_text)
|
||||
words = passage_text.split()
|
||||
near_miss = " ".join(words[:11]) + " released to the audience."
|
||||
verdict = anchor_quote(near_miss, [passage])
|
||||
assert 0.80 <= verdict.ratio < 0.85, f"expected weak band, got {verdict.ratio}"
|
||||
assert verdict.matched is False
|
||||
assert verdict.weak is True, f"near-threshold must flag weak, got {verdict.ratio}"
|
||||
|
||||
|
||||
def test_attack_fully_fabricated_report_fails_export() -> None:
|
||||
store = EvidenceStore()
|
||||
real = store.add_passage("https://example.org/real", "A genuine stored passage " * 10)
|
||||
fabricated_report = (
|
||||
"# Report\n\nClaim [p-1].\n\nClaim [p-7]. Claim [p-42].\n"
|
||||
)
|
||||
ledger = build_ledger(
|
||||
run_id="attack002",
|
||||
report_text=fabricated_report,
|
||||
evidence=store,
|
||||
liveness={"https://example.org/real": LivenessStatus.LIVE},
|
||||
visited_at=datetime.now(UTC),
|
||||
)
|
||||
passed, failures = liveness_export_gate(ledger)
|
||||
assert passed is False, "fully-fabricated report must fail the export gate"
|
||||
assert failures == []
|
||||
assert ledger.checklist.unanchored_claims >= 2
|
||||
del real
|
||||
|
||||
|
||||
def test_export_round_trip_json_validates_into_backend_model() -> None:
|
||||
from researchx.trust.ledger import CitationLedger
|
||||
|
||||
store = EvidenceStore()
|
||||
store.add_passage("https://example.org/a", "A passage with real content " * 6)
|
||||
report = "# Report\n\nClaim [p-1].\n"
|
||||
ledger = build_ledger(
|
||||
run_id="attack003",
|
||||
report_text=report,
|
||||
evidence=store,
|
||||
liveness={"https://example.org/a": LivenessStatus.LIVE},
|
||||
visited_at=datetime.now(UTC),
|
||||
)
|
||||
exported = ledger.model_dump_json()
|
||||
restored = CitationLedger.model_validate_json(exported)
|
||||
assert restored == ledger
|
||||
checklist_md = export_checklist(ledger)
|
||||
assert "Anchoring gate" in checklist_md
|
||||
assert "PASS" in checklist_md
|
||||
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from researchx.adapters.fakes import CORPUS, FakeModelAdapter, FakeSearchAdapter
|
||||
from researchx.adapters.types import ChatResult
|
||||
from researchx.api.app import create_app
|
||||
|
||||
_PLAN = json.dumps(
|
||||
{"sub_questions": ["verification passes"], "source_universe": ["web"], "outline": ["intro"]}
|
||||
)
|
||||
_WRITER = "# Report\n\nClaim [p-1] and claim [p-2].\n"
|
||||
|
||||
|
||||
def _app(tmp_path: Path):
|
||||
return create_app(
|
||||
storage_dir=tmp_path,
|
||||
model_adapter=FakeModelAdapter(
|
||||
canned=[
|
||||
ChatResult(content=_PLAN, model="fake-planner"),
|
||||
ChatResult(content=_WRITER, model="fake-writer"),
|
||||
]
|
||||
),
|
||||
search_adapter=FakeSearchAdapter(CORPUS),
|
||||
)
|
||||
|
||||
|
||||
async def _client(tmp_path: Path) -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=_app(tmp_path)),
|
||||
base_url="http://testserver",
|
||||
)
|
||||
|
||||
|
||||
async def test_config_status(tmp_path: Path) -> None:
|
||||
async with await _client(tmp_path) as client:
|
||||
response = await client.get("/api/config")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["model_adapter"] == "FakeModelAdapter"
|
||||
assert body["cost_counters"]["runs_created"] == 0
|
||||
|
||||
|
||||
async def test_methodology_cards(tmp_path: Path) -> None:
|
||||
async with await _client(tmp_path) as client:
|
||||
response = await client.get("/api/methodologies")
|
||||
assert response.status_code == 200
|
||||
cards = response.json()
|
||||
slugs = [c["slug"] for c in cards]
|
||||
assert "rapid-answer" in slugs and "deep-report" in slugs
|
||||
for card in cards:
|
||||
assert card["eta_minutes"] > 0
|
||||
assert "estimate" in card["cost_estimate"]
|
||||
assert card["why_recommended"]
|
||||
assert card["dials"]["depth"] >= 1
|
||||
|
||||
|
||||
async def test_create_run_rejects_unknown_methodology(tmp_path: Path) -> None:
|
||||
async with await _client(tmp_path) as client:
|
||||
response = await client.post(
|
||||
"/api/runs", json={"question": "q", "methodology": "nonexistent"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def _await_status(client: httpx.AsyncClient, run_id: str, status: str) -> None:
|
||||
for _ in range(200):
|
||||
current = (await client.get(f"/api/runs/{run_id}")).json()
|
||||
if current["status"] == status:
|
||||
return
|
||||
await asyncio.sleep(0.01)
|
||||
raise AssertionError(f"run never reached {status}: {current['status']}")
|
||||
|
||||
|
||||
async def test_run_lifecycle_over_http(tmp_path: Path) -> None:
|
||||
async with await _client(tmp_path) as client:
|
||||
created = await client.post(
|
||||
"/api/runs", json={"question": "How do engines run?", "methodology": "rapid-answer"}
|
||||
)
|
||||
assert created.status_code == 201
|
||||
run_id = created.json()["id"]
|
||||
|
||||
await _await_status(client, run_id, "awaiting_approval")
|
||||
approved = await client.post(f"/api/runs/{run_id}/approve")
|
||||
assert approved.status_code == 200
|
||||
|
||||
await _await_status(client, run_id, "done")
|
||||
report = await client.get(f"/api/runs/{run_id}/report")
|
||||
assert report.status_code == 200
|
||||
assert "[p-1]" in report.json()["markdown"]
|
||||
|
||||
ledger = await client.get(f"/api/runs/{run_id}/ledger")
|
||||
assert ledger.status_code == 200
|
||||
assert ledger.json()["checklist"]["citations_checked"] >= 1
|
||||
|
||||
config = (await client.get("/api/config")).json()
|
||||
assert config["cost_counters"]["runs_created"] == 1
|
||||
assert config["cost_counters"]["runs_completed"] == 1
|
||||
|
||||
|
||||
async def test_missing_run_404(tmp_path: Path) -> None:
|
||||
async with await _client(tmp_path) as client:
|
||||
assert (await client.get("/api/runs/zzzz9999")).status_code == 404
|
||||
assert (await client.post("/api/runs/zzzz9999/approve")).status_code == 404
|
||||
assert (await client.get("/api/runs/zzzz9999/events")).status_code == 404
|
||||
|
||||
|
||||
async def test_sse_stream_replays_events(tmp_path: Path) -> None:
|
||||
client = await _client(tmp_path)
|
||||
try:
|
||||
created = await client.post(
|
||||
"/api/runs", json={"question": "q", "methodology": "rapid-answer"}
|
||||
)
|
||||
run_id = created.json()["id"]
|
||||
await _await_status(client, run_id, "awaiting_approval")
|
||||
await client.post(f"/api/runs/{run_id}/approve")
|
||||
await _await_status(client, run_id, "done")
|
||||
|
||||
collected = b""
|
||||
async with client.stream("GET", f"/api/runs/{run_id}/events") as response:
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("text/event-stream")
|
||||
async for chunk in response.aiter_bytes():
|
||||
collected += chunk
|
||||
if b"run_done" in collected:
|
||||
break
|
||||
text = collected.decode()
|
||||
assert "event: run_started" in text
|
||||
assert "event: run_done" in text
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
|
||||
async def test_pause_resume_cancel_over_http(tmp_path: Path) -> None:
|
||||
async with await _client(tmp_path) as client:
|
||||
created = await client.post(
|
||||
"/api/runs", json={"question": "q", "methodology": "rapid-answer"}
|
||||
)
|
||||
run_id = created.json()["id"]
|
||||
await _await_status(client, run_id, "awaiting_approval")
|
||||
|
||||
paused = await client.post(f"/api/runs/{run_id}/pause")
|
||||
assert paused.status_code == 200
|
||||
resumed = await client.post(f"/api/runs/{run_id}/resume")
|
||||
assert resumed.status_code == 200
|
||||
|
||||
cancelled = await client.post(f"/api/runs/{run_id}/cancel")
|
||||
assert cancelled.status_code == 200
|
||||
for _ in range(200):
|
||||
status = (await client.get(f"/api/runs/{run_id}")).json()["status"]
|
||||
if status == "cancelled":
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
assert status == "cancelled"
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Opt-in live smoke tier (G-004): real-adapter probes, keyed only.
|
||||
|
||||
These tests NEVER run in the zero-key CI suite: they are marked
|
||||
`live` and skip cleanly when OLLAMA_API_KEY / TAVILY_API_KEY are
|
||||
absent from the secrets scope. Run explicitly with:
|
||||
|
||||
uv run pytest -m live
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from researchx.adapters.capability_probe import probe_model_adapter
|
||||
from researchx.adapters.ollama import OllamaModelAdapter
|
||||
from researchx.adapters.secrets import EnvFileSecretsProvider
|
||||
from researchx.adapters.tavily import TavilySearchAdapter
|
||||
from researchx.trust.liveness import LivenessStatus, check_liveness
|
||||
|
||||
pytestmark = pytest.mark.live
|
||||
|
||||
_SECRETS_PATHS = [
|
||||
Path.cwd() / ".ciagent" / ".env.secrets",
|
||||
Path.cwd().parents[1] / ".ciagent" / ".env.secrets",
|
||||
]
|
||||
|
||||
|
||||
def _secrets() -> EnvFileSecretsProvider:
|
||||
return EnvFileSecretsProvider(paths=_SECRETS_PATHS)
|
||||
|
||||
|
||||
def _needs_key(key: str) -> str:
|
||||
value = _secrets().resolve(key) or os.environ.get(key)
|
||||
if not value:
|
||||
pytest.skip(f"{key} not present — live tier is opt-in")
|
||||
return value
|
||||
|
||||
|
||||
async def test_live_ollama_probe_and_chat() -> None:
|
||||
_needs_key("OLLAMA_API_KEY")
|
||||
adapter = OllamaModelAdapter(
|
||||
_secrets(), planner_model="gpt-oss:20b", writer_model="gpt-oss:20b"
|
||||
)
|
||||
probe = await probe_model_adapter(adapter)
|
||||
assert probe.endpoint in ("https://ollama.com/v1", "http://localhost:11434/v1")
|
||||
assert probe.context_window <= 8192
|
||||
assert probe.embedding_batch == 8
|
||||
|
||||
|
||||
async def test_live_tavily_search() -> None:
|
||||
_needs_key("TAVILY_API_KEY")
|
||||
adapter = TavilySearchAdapter(_secrets())
|
||||
results = await adapter.search("deep research agent verification", max_results=3)
|
||||
assert 1 <= len(results) <= 3
|
||||
assert all(r.url.startswith("http") for r in results)
|
||||
|
||||
|
||||
async def test_live_liveness_against_real_page() -> None:
|
||||
status = await check_liveness("https://example.org/")
|
||||
assert status in (LivenessStatus.LIVE, LivenessStatus.STALE, LivenessStatus.DEAD)
|
||||
void_httpx = httpx.AsyncClient
|
||||
del void_httpx
|
||||
Generated
+2
@@ -521,6 +521,7 @@ dependencies = [
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
@@ -538,6 +539,7 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "mypy", specifier = ">=1.11" },
|
||||
{ name = "pytest", specifier = ">=8" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.23" },
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "ResearchX API",
|
||||
"version": "0.2.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/config": {
|
||||
"get": {
|
||||
"summary": "Config Status",
|
||||
"operationId": "config_status_api_config_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConfigStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/methodologies": {
|
||||
"get": {
|
||||
"summary": "Methodologies",
|
||||
"operationId": "methodologies_api_methodologies_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MethodologyCard"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Response Methodologies Api Methodologies Get"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs": {
|
||||
"post": {
|
||||
"summary": "Create Run",
|
||||
"operationId": "create_run_api_runs_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateRunBody"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}": {
|
||||
"get": {
|
||||
"summary": "Get Run",
|
||||
"operationId": "get_run_api_runs__run_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/approve": {
|
||||
"post": {
|
||||
"summary": "Approve Run",
|
||||
"operationId": "approve_run_api_runs__run_id__approve_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApproveBody"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/pause": {
|
||||
"post": {
|
||||
"summary": "Pause Run",
|
||||
"operationId": "pause_run_api_runs__run_id__pause_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/resume": {
|
||||
"post": {
|
||||
"summary": "Resume Run",
|
||||
"operationId": "resume_run_api_runs__run_id__resume_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/cancel": {
|
||||
"post": {
|
||||
"summary": "Cancel Run",
|
||||
"operationId": "cancel_run_api_runs__run_id__cancel_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/events": {
|
||||
"get": {
|
||||
"summary": "Run Events",
|
||||
"operationId": "run_events_api_runs__run_id__events_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/report": {
|
||||
"get": {
|
||||
"summary": "Run Report",
|
||||
"operationId": "run_report_api_runs__run_id__report_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response Run Report Api Runs Run Id Report Get"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/runs/{run_id}/ledger": {
|
||||
"get": {
|
||||
"summary": "Run Ledger",
|
||||
"operationId": "run_ledger_api_runs__run_id__ledger_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Run Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response Run Ledger Api Runs Run Id Ledger Get"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ApproveBody": {
|
||||
"properties": {
|
||||
"plan": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Plan"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "ApproveBody"
|
||||
},
|
||||
"ConfigStatus": {
|
||||
"properties": {
|
||||
"model_adapter": {
|
||||
"type": "string",
|
||||
"title": "Model Adapter"
|
||||
},
|
||||
"search_adapter": {
|
||||
"type": "string",
|
||||
"title": "Search Adapter"
|
||||
},
|
||||
"storage_dir": {
|
||||
"type": "string",
|
||||
"title": "Storage Dir"
|
||||
},
|
||||
"probe": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Probe"
|
||||
},
|
||||
"cost_counters": {
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Cost Counters"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model_adapter",
|
||||
"search_adapter",
|
||||
"storage_dir",
|
||||
"cost_counters"
|
||||
],
|
||||
"title": "ConfigStatus"
|
||||
},
|
||||
"CreateRunBody": {
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Question"
|
||||
},
|
||||
"context": {
|
||||
"type": "string",
|
||||
"title": "Context",
|
||||
"default": ""
|
||||
},
|
||||
"methodology": {
|
||||
"type": "string",
|
||||
"title": "Methodology"
|
||||
},
|
||||
"dial_overrides": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Dial Overrides"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"question",
|
||||
"methodology"
|
||||
],
|
||||
"title": "CreateRunBody"
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Detail"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError"
|
||||
},
|
||||
"MethodologyCard": {
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"title": "Slug"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"title": "Description"
|
||||
},
|
||||
"eta_minutes": {
|
||||
"type": "integer",
|
||||
"title": "Eta Minutes"
|
||||
},
|
||||
"cost_estimate": {
|
||||
"type": "string",
|
||||
"title": "Cost Estimate"
|
||||
},
|
||||
"why_recommended": {
|
||||
"type": "string",
|
||||
"title": "Why Recommended"
|
||||
},
|
||||
"output_shape": {
|
||||
"type": "string",
|
||||
"title": "Output Shape"
|
||||
},
|
||||
"dials": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Dials"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"slug",
|
||||
"name",
|
||||
"description",
|
||||
"eta_minutes",
|
||||
"cost_estimate",
|
||||
"why_recommended",
|
||||
"output_shape",
|
||||
"dials"
|
||||
],
|
||||
"title": "MethodologyCard"
|
||||
},
|
||||
"RunSummary": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"title": "Id"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"title": "Status"
|
||||
},
|
||||
"question": {
|
||||
"type": "string",
|
||||
"title": "Question"
|
||||
},
|
||||
"methodology": {
|
||||
"type": "string",
|
||||
"title": "Methodology"
|
||||
},
|
||||
"plan": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Plan"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"title": "Started At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"question",
|
||||
"methodology",
|
||||
"started_at"
|
||||
],
|
||||
"title": "RunSummary"
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Error Type"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"title": "ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,11 @@
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "15.5.6",
|
||||
"eventsource": "^5.1.2",
|
||||
"globals": "^17.0.0",
|
||||
"jsdom": "26.1.0",
|
||||
"openapi-fetch": "^0.13.8",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"tailwindcss": "4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.70.1",
|
||||
|
||||
Generated
+305
-123
@@ -168,7 +168,7 @@ importers:
|
||||
dependencies:
|
||||
next:
|
||||
specifier: 15.5.6
|
||||
version: 15.5.6(@babel/core@7.29.7(supports-color@7.2.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
version: 15.5.6(@babel/core@7.29.7(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
react:
|
||||
specifier: 19.2.8
|
||||
version: 19.2.8
|
||||
@@ -178,7 +178,7 @@ importers:
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3.3.7
|
||||
version: 3.3.7(supports-color@7.2.0)
|
||||
version: 3.3.7(supports-color@10.2.2)
|
||||
'@tailwindcss/postcss':
|
||||
specifier: 4.1.18
|
||||
version: 4.1.18
|
||||
@@ -205,19 +205,28 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.5)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: 4.7.0
|
||||
version: 4.7.0(supports-color@7.2.0)(vite@7.3.6(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2))
|
||||
version: 4.7.0(supports-color@10.2.2)(vite@7.3.6(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2))
|
||||
eslint:
|
||||
specifier: ^9.39.5
|
||||
version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
version: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
eslint-config-next:
|
||||
specifier: 15.5.6
|
||||
version: 15.5.6(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
version: 15.5.6(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
eventsource:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
globals:
|
||||
specifier: ^17.0.0
|
||||
version: 17.12.0
|
||||
jsdom:
|
||||
specifier: 26.1.0
|
||||
version: 26.1.0(supports-color@7.2.0)
|
||||
version: 26.1.0(supports-color@10.2.2)
|
||||
openapi-fetch:
|
||||
specifier: ^0.13.8
|
||||
version: 0.13.8
|
||||
openapi-typescript:
|
||||
specifier: ^7.13.0
|
||||
version: 7.13.0(typescript@5.9.3)
|
||||
tailwindcss:
|
||||
specifier: 4.1.18
|
||||
version: 4.1.18
|
||||
@@ -226,10 +235,10 @@ importers:
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.70.1
|
||||
version: 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
version: 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
vitest:
|
||||
specifier: 3.2.7
|
||||
version: 3.2.7(@types/node@24.3.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@7.2.0))(lightningcss@1.30.2)(supports-color@7.2.0)
|
||||
version: 3.2.7(@types/node@24.3.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(lightningcss@1.30.2)(supports-color@10.2.2)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -841,6 +850,16 @@ packages:
|
||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||
engines: {node: '>=12.4.0'}
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
|
||||
|
||||
'@redocly/config@0.22.0':
|
||||
resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
|
||||
|
||||
'@redocly/openapi-core@1.34.20':
|
||||
resolution: {integrity: sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw==}
|
||||
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
@@ -1390,6 +1409,10 @@ packages:
|
||||
ajv@6.15.0:
|
||||
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1482,6 +1505,9 @@ packages:
|
||||
brace-expansion@1.1.21:
|
||||
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
||||
|
||||
brace-expansion@2.1.7:
|
||||
resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
|
||||
|
||||
brace-expansion@5.0.12:
|
||||
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -1526,6 +1552,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
change-case@5.4.4:
|
||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||
|
||||
check-error@2.1.3:
|
||||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -1540,6 +1569,9 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
@@ -1826,6 +1858,14 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
eventsource-parser@4.1.1:
|
||||
resolution: {integrity: sha512-zaWNqeSLeyzEAAt2Q6aDBRDRk/VnPmsKqvToyGvv6aahvLv71A4Jw7jG93Hckf11zXurbqgsffvpSgKPoeU1Qw==}
|
||||
engines: {node: '>=22.12'}
|
||||
|
||||
eventsource@5.1.2:
|
||||
resolution: {integrity: sha512-VJnwqqp+fOT3WDkjnUy49Zuyh48+kH0JdJApZkslrRQyx35zIZJeJYJl0+a+LrTzh5TFv6MdmXnBDv71UXFyWQ==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
expect-type@1.4.0:
|
||||
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -2006,6 +2046,10 @@ packages:
|
||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
index-to-position@1.2.0:
|
||||
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2134,6 +2178,10 @@ packages:
|
||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||
hasBin: true
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -2164,6 +2212,9 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -2318,6 +2369,10 @@ packages:
|
||||
minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
|
||||
minimatch@5.1.9:
|
||||
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
@@ -2402,6 +2457,18 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
openapi-fetch@0.13.8:
|
||||
resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
openapi-typescript@7.13.0:
|
||||
resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2422,6 +2489,10 @@ packages:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
@@ -2454,6 +2525,10 @@ packages:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2515,6 +2590,10 @@ packages:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2684,6 +2763,10 @@ packages:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2759,6 +2842,10 @@ packages:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2803,6 +2890,9 @@ packages:
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
uri-js-replace@1.0.1:
|
||||
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@@ -2952,6 +3042,13 @@ packages:
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yaml-ast-parser@0.0.43:
|
||||
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2978,20 +3075,20 @@ snapshots:
|
||||
|
||||
'@babel/compat-data@7.29.7': {}
|
||||
|
||||
'@babel/core@7.29.7(supports-color@7.2.0)':
|
||||
'@babel/core@7.29.7(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.8
|
||||
'@babel/helper-compilation-targets': 7.29.7
|
||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
'@babel/helpers': 7.29.7
|
||||
'@babel/parser': 7.29.9
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/traverse': 7.29.8(supports-color@7.2.0)
|
||||
'@babel/traverse': 7.29.8(supports-color@10.2.2)
|
||||
'@babel/types': 7.29.8
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@@ -3016,19 +3113,19 @@ snapshots:
|
||||
|
||||
'@babel/helper-globals@7.29.7': {}
|
||||
|
||||
'@babel/helper-module-imports@7.29.7(supports-color@7.2.0)':
|
||||
'@babel/helper-module-imports@7.29.7(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.8(supports-color@7.2.0)
|
||||
'@babel/traverse': 7.29.8(supports-color@10.2.2)
|
||||
'@babel/types': 7.29.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
'@babel/traverse': 7.29.8(supports-color@7.2.0)
|
||||
'@babel/traverse': 7.29.8(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3049,14 +3146,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.29.8
|
||||
|
||||
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))':
|
||||
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
|
||||
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))':
|
||||
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
|
||||
'@babel/runtime@7.29.7': {}
|
||||
@@ -3067,7 +3164,7 @@ snapshots:
|
||||
'@babel/parser': 7.29.9
|
||||
'@babel/types': 7.29.8
|
||||
|
||||
'@babel/traverse@7.29.8(supports-color@7.2.0)':
|
||||
'@babel/traverse@7.29.8(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.8
|
||||
@@ -3075,7 +3172,7 @@ snapshots:
|
||||
'@babel/parser': 7.29.9
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.8
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3203,17 +3300,17 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))':
|
||||
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))':
|
||||
dependencies:
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/config-array@0.21.2(supports-color@7.2.0)':
|
||||
'@eslint/config-array@0.21.2(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
minimatch: 3.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -3226,10 +3323,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/eslintrc@3.3.7(supports-color@7.2.0)':
|
||||
'@eslint/eslintrc@3.3.7(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
ajv: 6.15.0
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@@ -3435,6 +3532,29 @@ snapshots:
|
||||
|
||||
'@nolyfill/is-core-module@1.0.39': {}
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js-replace: 1.0.1
|
||||
|
||||
'@redocly/config@0.22.0': {}
|
||||
|
||||
'@redocly/openapi-core@1.34.20(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@redocly/ajv': 8.11.2
|
||||
'@redocly/config': 0.22.0
|
||||
colorette: 1.4.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||
js-levenshtein: 1.1.6
|
||||
js-yaml: 4.3.2
|
||||
minimatch: 5.1.9
|
||||
pluralize: 8.0.0
|
||||
yaml-ast-parser: 0.0.43
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.5':
|
||||
@@ -3677,15 +3797,15 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.70.1
|
||||
'@typescript-eslint/type-utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
ignore: 7.0.10
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
@@ -3693,23 +3813,23 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.70.1
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.70.1(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/project-service@8.70.1(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -3723,13 +3843,13 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -3737,13 +3857,13 @@ snapshots:
|
||||
|
||||
'@typescript-eslint/types@8.70.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.70.1(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/typescript-estree@8.70.1(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/project-service': 8.70.1(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
minimatch: 10.2.6
|
||||
semver: 7.8.5
|
||||
tinyglobby: 0.2.17
|
||||
@@ -3752,13 +3872,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
||||
'@typescript-eslint/scope-manager': 8.70.1
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -3838,11 +3958,11 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
||||
optional: true
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(supports-color@7.2.0)(vite@7.3.6(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2))':
|
||||
'@vitejs/plugin-react@4.7.0(supports-color@10.2.2)(vite@7.3.6(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
||||
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
@@ -3907,6 +4027,8 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
@@ -4015,6 +4137,10 @@ snapshots:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.1.7:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
brace-expansion@5.0.12:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
@@ -4067,6 +4193,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
change-case@5.4.4: {}
|
||||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
@@ -4077,6 +4205,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
@@ -4121,17 +4251,17 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
debug@3.2.7(supports-color@7.2.0):
|
||||
debug@3.2.7(supports-color@10.2.2):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
supports-color: 10.2.2
|
||||
|
||||
debug@4.4.3(supports-color@7.2.0):
|
||||
debug@4.4.3(supports-color@10.2.2):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
supports-color: 10.2.2
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
@@ -4326,19 +4456,19 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-next@15.5.6(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3):
|
||||
eslint-config-next@15.5.6(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.5.6
|
||||
'@rushstack/eslint-patch': 1.16.1
|
||||
'@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
|
||||
'@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@10.2.2)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -4346,52 +4476,52 @@ snapshots:
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-node@0.3.10(supports-color@7.2.0):
|
||||
eslint-import-resolver-node@0.3.10(supports-color@10.2.2):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@7.2.0)
|
||||
debug: 3.2.7(supports-color@10.2.2)
|
||||
is-core-module: 2.17.0
|
||||
resolve: 2.0.0-next.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
get-tsconfig: 4.14.3
|
||||
is-bun-module: 2.0.0
|
||||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.17
|
||||
unrs-resolver: 1.12.2
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@7.2.0)
|
||||
debug: 3.2.7(supports-color@10.2.2)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@10.2.2)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.2.0
|
||||
array.prototype.findlastindex: 1.2.6
|
||||
array.prototype.flat: 1.3.3
|
||||
array.prototype.flatmap: 1.3.3
|
||||
debug: 3.2.7(supports-color@7.2.0)
|
||||
debug: 3.2.7(supports-color@10.2.2)
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
|
||||
eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@10.2.2)
|
||||
eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
hasown: 2.0.4
|
||||
is-core-module: 2.17.0
|
||||
is-glob: 4.0.3
|
||||
@@ -4403,13 +4533,13 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.10
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)):
|
||||
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)):
|
||||
dependencies:
|
||||
aria-query: 5.3.2
|
||||
array-includes: 3.2.0
|
||||
@@ -4419,7 +4549,7 @@ snapshots:
|
||||
axobject-query: 4.1.0
|
||||
damerau-levenshtein: 1.0.8
|
||||
emoji-regex: 9.2.2
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
hasown: 2.0.4
|
||||
jsx-ast-utils: 3.3.5
|
||||
language-tags: 1.0.9
|
||||
@@ -4428,11 +4558,11 @@ snapshots:
|
||||
safe-regex-test: 1.1.0
|
||||
string.prototype.includes: 2.0.1
|
||||
|
||||
eslint-plugin-react-hooks@5.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)):
|
||||
eslint-plugin-react-hooks@5.2.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)):
|
||||
dependencies:
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
|
||||
eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)):
|
||||
eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2)):
|
||||
dependencies:
|
||||
array-includes: 3.2.0
|
||||
array.prototype.findlast: 1.2.5
|
||||
@@ -4440,7 +4570,7 @@ snapshots:
|
||||
array.prototype.tosorted: 1.1.4
|
||||
doctrine: 2.1.0
|
||||
es-iterator-helpers: 1.4.0
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
estraverse: 5.3.0
|
||||
hasown: 2.0.4
|
||||
jsx-ast-utils: 3.3.5
|
||||
@@ -4465,14 +4595,14 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0):
|
||||
eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.21.2(supports-color@7.2.0)
|
||||
'@eslint/config-array': 0.21.2(supports-color@10.2.2)
|
||||
'@eslint/config-helpers': 0.4.2
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/eslintrc': 3.3.7(supports-color@7.2.0)
|
||||
'@eslint/eslintrc': 3.3.7(supports-color@10.2.2)
|
||||
'@eslint/js': 9.39.5
|
||||
'@eslint/plugin-kit': 0.4.1
|
||||
'@humanfs/node': 0.16.8
|
||||
@@ -4482,7 +4612,7 @@ snapshots:
|
||||
ajv: 6.15.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
@@ -4528,6 +4658,12 @@ snapshots:
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
eventsource-parser@4.1.1: {}
|
||||
|
||||
eventsource@5.1.2:
|
||||
dependencies:
|
||||
eventsource-parser: 4.1.1
|
||||
|
||||
expect-type@1.4.0: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
@@ -4674,17 +4810,17 @@ snapshots:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
http-proxy-agent@7.0.2(supports-color@7.2.0):
|
||||
http-proxy-agent@7.0.2(supports-color@10.2.2):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@7.2.0):
|
||||
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -4705,6 +4841,8 @@ snapshots:
|
||||
|
||||
indent-string@4.0.0: {}
|
||||
|
||||
index-to-position@1.2.0: {}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -4844,6 +4982,8 @@ snapshots:
|
||||
|
||||
jiti@2.7.0: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
@@ -4852,14 +4992,14 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@26.1.0(supports-color@7.2.0):
|
||||
jsdom@26.1.0(supports-color@10.2.2):
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2(supports-color@7.2.0)
|
||||
https-proxy-agent: 7.0.6(supports-color@7.2.0)
|
||||
http-proxy-agent: 7.0.2(supports-color@10.2.2)
|
||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.28
|
||||
parse5: 7.3.0
|
||||
@@ -4885,6 +5025,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@1.0.2:
|
||||
@@ -5007,6 +5149,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.21
|
||||
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.7
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -5017,7 +5163,7 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next@15.5.6(@babel/core@7.29.7(supports-color@7.2.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
next@15.5.6(@babel/core@7.29.7(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
dependencies:
|
||||
'@next/env': 15.5.6
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -5025,7 +5171,7 @@ snapshots:
|
||||
postcss: 8.4.31
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.8)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.5.6
|
||||
'@next/swc-darwin-x64': 15.5.6
|
||||
@@ -5093,6 +5239,22 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.2
|
||||
|
||||
openapi-fetch@0.13.8:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
openapi-typescript@7.13.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@redocly/openapi-core': 1.34.20(supports-color@10.2.2)
|
||||
ansi-colors: 4.1.3
|
||||
change-case: 5.4.4
|
||||
parse-json: 8.3.0
|
||||
supports-color: 10.2.2
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -5121,6 +5283,12 @@ snapshots:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
index-to-position: 1.2.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
@@ -5141,6 +5309,8 @@ snapshots:
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
@@ -5211,6 +5381,8 @@ snapshots:
|
||||
gopd: 1.2.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -5461,12 +5633,14 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8):
|
||||
styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.8):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.8
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
@@ -5530,6 +5704,8 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -5562,13 +5738,13 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typescript-eslint@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3):
|
||||
typescript-eslint@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
'@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -5617,14 +5793,16 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uri-js-replace@1.0.1: {}
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
vite-node@3.2.4(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2)(supports-color@7.2.0):
|
||||
vite-node@3.2.4(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.6(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2)
|
||||
@@ -5656,7 +5834,7 @@ snapshots:
|
||||
jiti: 2.7.0
|
||||
lightningcss: 1.30.2
|
||||
|
||||
vitest@3.2.7(@types/node@24.3.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@7.2.0))(lightningcss@1.30.2)(supports-color@7.2.0):
|
||||
vitest@3.2.7(@types/node@24.3.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(lightningcss@1.30.2)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.7
|
||||
@@ -5667,7 +5845,7 @@ snapshots:
|
||||
'@vitest/spy': 3.2.7
|
||||
'@vitest/utils': 3.2.7
|
||||
chai: 5.3.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
expect-type: 1.4.0
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
@@ -5679,11 +5857,11 @@ snapshots:
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.3.6(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2)
|
||||
vite-node: 3.2.4(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2)(supports-color@7.2.0)
|
||||
vite-node: 3.2.4(@types/node@24.3.1)(jiti@2.7.0)(lightningcss@1.30.2)(supports-color@10.2.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.3.1
|
||||
jsdom: 26.1.0(supports-color@7.2.0)
|
||||
jsdom: 26.1.0(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
@@ -5775,4 +5953,8 @@ snapshots:
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
UserProfile,
|
||||
} from "@/lib/types";
|
||||
import { MockApiClient } from "@/lib/api/mock/mock-client";
|
||||
import { GeneratedApiClient } from "@/lib/api/live-client";
|
||||
|
||||
export type { Report, ReportSection } from "@/lib/types";
|
||||
export type {
|
||||
@@ -66,7 +67,8 @@ export interface CreateRunInput {
|
||||
|
||||
/**
|
||||
* Subscription handle for the live trace stream (EventSource-shaped).
|
||||
* v0.2 backs this with SSE; pause/resume stay client-side concerns.
|
||||
* v0.2: backed by SSE in live mode; client-side pause/resume kept
|
||||
* (server-side pause/resume added via the P3 extension below).
|
||||
*/
|
||||
export interface RunEventSubscription {
|
||||
/** Stop the stream and release its timers (idempotent). */
|
||||
@@ -77,6 +79,18 @@ export interface RunEventSubscription {
|
||||
resume(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.2 P3 extension (grill G-003 — the single planned interface
|
||||
* addition): server-side pause/resume of a running engine across
|
||||
* restarts. Mock mode ignores it; live mode hits the backend.
|
||||
*/
|
||||
export interface RunLifecycle {
|
||||
/** Pause the engine server-side (durable across restarts). */
|
||||
pauseRun(id: string): Promise<ResearchRun>;
|
||||
/** Resume a server-side-paused run. */
|
||||
resumeRun(id: string): Promise<ResearchRun>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The backend-swappable contract. Every screen talks to this — never
|
||||
* to a concrete client, never to the database, never to the network.
|
||||
@@ -181,7 +195,29 @@ export interface ApiClient {
|
||||
|
||||
/**
|
||||
* The app-wide singleton. v0.1: MockApiClient (fixture data + scripted
|
||||
* event replay, zero API keys). v0.2 P3: rebind to the OpenAPI-generated
|
||||
* client — this line is the only thing that changes.
|
||||
* event replay, zero API keys). v0.2 P3 (REQ-26/REQ-54): the binding
|
||||
* switches to the OpenAPI-generated client in live mode — selected by
|
||||
* NEXT_PUBLIC_RESEARCHX_API_MODE=live (mock stays the default). Live mode covers
|
||||
* the core research loop only (G-002); other surfaces stay mock-backed.
|
||||
*/
|
||||
export const api: ApiClient = new MockApiClient();
|
||||
class MockApiClientWithLifecycle extends MockApiClient implements RunLifecycle {
|
||||
async pauseRun(id: string): Promise<ResearchRun> {
|
||||
return this.getRun(id);
|
||||
}
|
||||
async resumeRun(id: string): Promise<ResearchRun> {
|
||||
return this.getRun(id);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBinding(): ApiClient & RunLifecycle {
|
||||
const mode =
|
||||
process.env.NEXT_PUBLIC_RESEARCHX_API_MODE ??
|
||||
process.env.RESEARCHX_API_MODE ??
|
||||
"mock";
|
||||
if (mode === "live") {
|
||||
return new GeneratedApiClient() as unknown as ApiClient & RunLifecycle;
|
||||
}
|
||||
return new MockApiClientWithLifecycle();
|
||||
}
|
||||
|
||||
export const api: ApiClient & RunLifecycle = resolveBinding();
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*
|
||||
* E2E happy path on the REAL stack (REQ-17): new user → research
|
||||
* running in ≤3 clicks using defaults only.
|
||||
*
|
||||
* The live ApiClient binding talks to a real FastAPI app (engine +
|
||||
* fakes) through httpx's ASGI transport wired in as fetch. This is
|
||||
* the full boundary → API → engine → trust-loop loop — no mocks in
|
||||
* the data path. Click budget: submit question (1), pick methodology
|
||||
* (2), approve plan (3).
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import { EventSource as EventSourcePolyfill } from "eventsource";
|
||||
|
||||
const g = globalThis as unknown as { EventSource?: unknown };
|
||||
if (!g.EventSource) {
|
||||
g.EventSource = EventSourcePolyfill;
|
||||
}
|
||||
|
||||
import { GeneratedApiClient } from "@/lib/api/live-client";
|
||||
|
||||
type Lifecycle = { pauseRun(id: string): Promise<unknown> };
|
||||
|
||||
let client: GeneratedApiClient & Lifecycle;
|
||||
let server: {
|
||||
ready: Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
app: unknown;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const { spawn } = await import("node:child_process");
|
||||
const { resolve } = await import("node:path");
|
||||
|
||||
const backendDir = resolve(process.cwd(), "..", "backend");
|
||||
const child = spawn(
|
||||
"/bin/bash",
|
||||
["-lc", "uv run python -m researchx.api.dev_bridge"],
|
||||
{
|
||||
cwd: backendDir,
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
},
|
||||
);
|
||||
|
||||
const ready = new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("bridge not ready in 60s")), 60_000);
|
||||
let buf = "";
|
||||
child.stdout!.on("data", (chunk: Buffer) => {
|
||||
buf += chunk.toString();
|
||||
if (buf.includes("READY")) {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
server = {
|
||||
app: child,
|
||||
ready,
|
||||
close: async () => {
|
||||
child.kill("SIGTERM");
|
||||
},
|
||||
};
|
||||
await ready;
|
||||
|
||||
const waitForPort = async (): Promise<void> => {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const probe = await fetch("http://127.0.0.1:8000/api/config");
|
||||
if (probe.ok) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// not listening yet
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
throw new Error("backend bridge never listened on 8000");
|
||||
};
|
||||
await waitForPort();
|
||||
|
||||
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const req = input instanceof Request ? input : new Request(input, init);
|
||||
const body = req.body ? await new Response(req.body).text() : undefined;
|
||||
const headers: Record<string, string> = {};
|
||||
req.headers.forEach((v, k) => {
|
||||
headers[k] = v;
|
||||
});
|
||||
const response = await fetch("http://127.0.0.1:8000" + new URL(req.url).pathname, {
|
||||
method: req.method,
|
||||
headers,
|
||||
...(body ? { body } : {}),
|
||||
});
|
||||
return response;
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
client = new GeneratedApiClient(fetchImpl) as GeneratedApiClient & Lifecycle;
|
||||
}, 90_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await server?.close?.();
|
||||
});
|
||||
|
||||
describe("E2E happy path — real stack, defaults only (REQ-17)", () => {
|
||||
it("runs research in ≤3 user actions", async () => {
|
||||
// Action budget: 1) submit question on Ask, 2) pick methodology on
|
||||
// Choose (defaults preselected — the plan requires zero dial fiddling),
|
||||
// 3) approve plan on Plan & Run.
|
||||
let userActions = 0;
|
||||
|
||||
const methodologies = await client.getMethodologies(); // catalog load — not a click
|
||||
expect(methodologies.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const question = "How do verification pipelines keep citations honest?";
|
||||
userActions += 1; // Ask: submit question
|
||||
const run = await client.createRun({
|
||||
question,
|
||||
methodologySlug: "rapid-answer",
|
||||
});
|
||||
expect(run.status).toBeOneOf(["planning", "awaiting_approval"]);
|
||||
|
||||
userActions += 2; // Choose: pick methodology + Plan & Run: approve
|
||||
let current = await client.getRun(run.id);
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (current.status === "planning" && Date.now() < deadline) {
|
||||
current = await client.getRun(run.id);
|
||||
}
|
||||
expect(current.plan.subQuestions.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const approved = await client.approvePlan(run.id);
|
||||
expect(approved.status).not.toBe("planning");
|
||||
|
||||
let done = false;
|
||||
const events: string[] = [];
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("run timed out")), 60_000);
|
||||
const subscription = client.getRunEvents(
|
||||
run.id,
|
||||
(event) => {
|
||||
events.push(event.type);
|
||||
},
|
||||
() => {
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
// Safety valve — cancel the subscription if the run hangs.
|
||||
setTimeout(() => {
|
||||
if (!done) {
|
||||
subscription.cancel();
|
||||
clearTimeout(timer);
|
||||
reject(new Error("no run_done within window"));
|
||||
}
|
||||
}, 55_000);
|
||||
});
|
||||
expect(done).toBe(true);
|
||||
expect(events).toContain("status");
|
||||
|
||||
const final = await client.getRun(run.id);
|
||||
expect(final.status).toBe("done");
|
||||
expect(userActions).toBeLessThanOrEqual(3);
|
||||
}, 90_000);
|
||||
|
||||
it("exports round-trip: markdown carries citation links, JSON validates (REQ-34)", async () => {
|
||||
const run = await client.createRun({
|
||||
question: "Export round-trip proof",
|
||||
methodologySlug: "rapid-answer",
|
||||
});
|
||||
const deadline = Date.now() + 30_000;
|
||||
let current = await client.getRun(run.id);
|
||||
while (current.status === "planning" && Date.now() < deadline) {
|
||||
current = await client.getRun(run.id);
|
||||
}
|
||||
await client.approvePlan(run.id);
|
||||
const doneDeadline = Date.now() + 60_000;
|
||||
while (current.status !== "done" && Date.now() < doneDeadline) {
|
||||
current = await client.getRun(run.id);
|
||||
}
|
||||
expect(current.status).toBe("done");
|
||||
|
||||
const markdown = await client.exportReport(run.id, "markdown");
|
||||
expect(markdown.filename.endsWith(".md")).toBe(true);
|
||||
expect(markdown.content).toContain("[p-");
|
||||
const linkRe = /\[https?:\/\/[^\]]+\]\(https?:\/\/[^)]+\)/;
|
||||
expect(markdown.content).toMatch(linkRe);
|
||||
|
||||
const json = await client.exportReport(run.id, "json");
|
||||
expect(json.filename.endsWith(".json")).toBe(true);
|
||||
const parsed = JSON.parse(json.content) as {
|
||||
run_id: string;
|
||||
entries: Array<{
|
||||
url: string;
|
||||
visited_at: string;
|
||||
passages: string[];
|
||||
health: string;
|
||||
authority_tier: string | number;
|
||||
}>;
|
||||
};
|
||||
expect(parsed.run_id).toBe(run.id);
|
||||
expect(parsed.entries.length).toBeGreaterThanOrEqual(1);
|
||||
for (const entry of parsed.entries) {
|
||||
expect(entry.url).toMatch(/^https?:\/\//);
|
||||
expect(entry.health).toBeOneOf(["live", "stale", "dead"]);
|
||||
expect(Array.isArray(entry.passages)).toBe(true);
|
||||
}
|
||||
}, 90_000);
|
||||
});
|
||||
+628
@@ -0,0 +1,628 @@
|
||||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api/config": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Config Status */
|
||||
get: operations["config_status_api_config_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/methodologies": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Methodologies */
|
||||
get: operations["methodologies_api_methodologies_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Create Run */
|
||||
post: operations["create_run_api_runs_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get Run */
|
||||
get: operations["get_run_api_runs__run_id__get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/approve": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Approve Run */
|
||||
post: operations["approve_run_api_runs__run_id__approve_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/pause": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Pause Run */
|
||||
post: operations["pause_run_api_runs__run_id__pause_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/resume": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Resume Run */
|
||||
post: operations["resume_run_api_runs__run_id__resume_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/cancel": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Cancel Run */
|
||||
post: operations["cancel_run_api_runs__run_id__cancel_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/events": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Run Events */
|
||||
get: operations["run_events_api_runs__run_id__events_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/report": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Run Report */
|
||||
get: operations["run_report_api_runs__run_id__report_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/runs/{run_id}/ledger": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Run Ledger */
|
||||
get: operations["run_ledger_api_runs__run_id__ledger_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
/** ApproveBody */
|
||||
ApproveBody: {
|
||||
/** Plan */
|
||||
plan?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
/** ConfigStatus */
|
||||
ConfigStatus: {
|
||||
/** Model Adapter */
|
||||
model_adapter: string;
|
||||
/** Search Adapter */
|
||||
search_adapter: string;
|
||||
/** Storage Dir */
|
||||
storage_dir: string;
|
||||
/** Probe */
|
||||
probe?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
/** Cost Counters */
|
||||
cost_counters: {
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
/** CreateRunBody */
|
||||
CreateRunBody: {
|
||||
/** Question */
|
||||
question: string;
|
||||
/**
|
||||
* Context
|
||||
* @default
|
||||
*/
|
||||
context: string;
|
||||
/** Methodology */
|
||||
methodology: string;
|
||||
/** Dial Overrides */
|
||||
dial_overrides?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
/** HTTPValidationError */
|
||||
HTTPValidationError: {
|
||||
/** Detail */
|
||||
detail?: components["schemas"]["ValidationError"][];
|
||||
};
|
||||
/** MethodologyCard */
|
||||
MethodologyCard: {
|
||||
/** Slug */
|
||||
slug: string;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Description */
|
||||
description: string;
|
||||
/** Eta Minutes */
|
||||
eta_minutes: number;
|
||||
/** Cost Estimate */
|
||||
cost_estimate: string;
|
||||
/** Why Recommended */
|
||||
why_recommended: string;
|
||||
/** Output Shape */
|
||||
output_shape: string;
|
||||
/** Dials */
|
||||
dials: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
/** RunSummary */
|
||||
RunSummary: {
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Status */
|
||||
status: string;
|
||||
/** Question */
|
||||
question: string;
|
||||
/** Methodology */
|
||||
methodology: string;
|
||||
/** Plan */
|
||||
plan?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
/** Started At */
|
||||
started_at: string;
|
||||
};
|
||||
/** ValidationError */
|
||||
ValidationError: {
|
||||
/** Location */
|
||||
loc: (string | number)[];
|
||||
/** Message */
|
||||
msg: string;
|
||||
/** Error Type */
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
config_status_api_config_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ConfigStatus"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
methodologies_api_methodologies_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["MethodologyCard"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
create_run_api_runs_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateRunBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunSummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_run_api_runs__run_id__get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunSummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
approve_run_api_runs__run_id__approve_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApproveBody"] | null;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunSummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
pause_run_api_runs__run_id__pause_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunSummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
resume_run_api_runs__run_id__resume_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunSummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
cancel_run_api_runs__run_id__cancel_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunSummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
run_events_api_runs__run_id__events_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
run_report_api_runs__run_id__report_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
run_ledger_api_runs__run_id__ledger_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
run_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Boundary tests for the v0.2 P3 client swap (REQ-26, G-003).
|
||||
*
|
||||
* Live mode is contract-tested against a stubbed global fetch — no
|
||||
* network, no keys. Mock mode stays pinned by the existing suite.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { GeneratedApiClient } from "@/lib/api/live-client";
|
||||
import type { BackendRun } from "@/lib/api/test-helpers";
|
||||
|
||||
type RouteHandler = (url: string, init?: RequestInit) => { status: number; body: unknown };
|
||||
|
||||
function stubRoutes(routes: Record<string, RouteHandler>): typeof globalThis.fetch {
|
||||
return (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = input instanceof Request ? input.url : String(input);
|
||||
for (const [prefix, handler] of Object.entries(routes)) {
|
||||
if (url.startsWith(prefix)) {
|
||||
const { status, body } = handler(url, init);
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
return new Response(JSON.stringify({ detail: "not found" }), { status: 404 });
|
||||
}) as typeof globalThis.fetch;
|
||||
}
|
||||
|
||||
const backendRun: BackendRun = {
|
||||
id: "abcd2345",
|
||||
status: "awaiting_approval",
|
||||
question: "How do engines run?",
|
||||
methodology: "rapid-answer",
|
||||
plan: {
|
||||
sub_questions: ["verification passes"],
|
||||
source_universe: ["web"],
|
||||
outline: ["intro"],
|
||||
},
|
||||
started_at: "2026-09-27T12:00:00Z",
|
||||
};
|
||||
|
||||
describe("GeneratedApiClient (live binding)", () => {
|
||||
it("maps backend methodology cards to the frontend Methodology shape", async () => {
|
||||
const routes = {
|
||||
"http://127.0.0.1:8000/api/methodologies": (): { status: number; body: unknown } => ({
|
||||
status: 200,
|
||||
body: [
|
||||
{
|
||||
slug: "rapid-answer",
|
||||
name: "Rapid Answer",
|
||||
description: "One pass, one memo. Fastest path.",
|
||||
eta_minutes: 2,
|
||||
cost_estimate: "~5k tokens (estimate)",
|
||||
why_recommended: "Fastest path to a confident cited answer.",
|
||||
output_shape: "memo",
|
||||
dials: { depth: 1, breadth: 1 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
const client = new GeneratedApiClient(stubRoutes(routes));
|
||||
const cards = await client.getMethodologies();
|
||||
expect(cards).toHaveLength(1);
|
||||
expect(cards[0]?.slug).toBe("rapid-answer");
|
||||
expect(cards[0]?.etaMinutes).toBe(2);
|
||||
expect(cards[0]?.costEstimate).toContain("estimate");
|
||||
expect(cards[0]?.defaultDials).toEqual({ depth: 1, breadth: 1 });
|
||||
});
|
||||
|
||||
it("creates runs through the backend contract", async () => {
|
||||
const routes = {
|
||||
"http://127.0.0.1:8000/api/runs": (url: string, init?: RequestInit) => {
|
||||
const method =
|
||||
init?.method ?? (url.endsWith("/api/runs") ? "POST" : undefined);
|
||||
return {
|
||||
status: 201,
|
||||
body: method === "POST" ? backendRun : { detail: "wrong method" },
|
||||
};
|
||||
},
|
||||
};
|
||||
const client = new GeneratedApiClient(stubRoutes(routes));
|
||||
const run = await client.createRun({
|
||||
question: "How do engines run?",
|
||||
methodologySlug: "rapid-answer",
|
||||
});
|
||||
expect(run.id).toBe("abcd2345");
|
||||
expect(run.status).toBe("awaiting_approval");
|
||||
expect(run.plan.subQuestions).toEqual(["verification passes"]);
|
||||
expect(run.plan.methodologySlug).toBe("rapid-answer");
|
||||
});
|
||||
|
||||
it("approves, cancels, pauses, resumes via lifecycle routes", async () => {
|
||||
const routes = {
|
||||
"http://127.0.0.1:8000/api/runs/abcd2345/approve": () => ({
|
||||
status: 200,
|
||||
body: { ...backendRun, status: "searching" },
|
||||
}),
|
||||
"http://127.0.0.1:8000/api/runs/abcd2345/cancel": () => ({
|
||||
status: 200,
|
||||
body: { ...backendRun, status: "cancelled" },
|
||||
}),
|
||||
"http://127.0.0.1:8000/api/runs/abcd2345/pause": () => ({
|
||||
status: 200,
|
||||
body: { ...backendRun, status: "searching" },
|
||||
}),
|
||||
"http://127.0.0.1:8000/api/runs/abcd2345/resume": () => ({
|
||||
status: 200,
|
||||
body: { ...backendRun, status: "verifying" },
|
||||
}),
|
||||
};
|
||||
const client = new GeneratedApiClient(stubRoutes(routes));
|
||||
expect((await client.approvePlan("abcd2345")).status).toBe("searching");
|
||||
expect((await client.cancelRun("abcd2345")).status).toBe("cancelled");
|
||||
expect((await client.pauseRun("abcd2345")).status).toBe("searching");
|
||||
expect((await client.resumeRun("abcd2345")).status).toBe("verifying");
|
||||
});
|
||||
|
||||
it("maps the ledger to frontend citations with health + tier", async () => {
|
||||
const routes = {
|
||||
"http://127.0.0.1:8000/api/runs/abcd2345/ledger": () => ({
|
||||
status: 200,
|
||||
body: {
|
||||
run_id: "abcd2345",
|
||||
generated_at: "2026-09-27T12:01:00Z",
|
||||
entries: [
|
||||
{
|
||||
url: "https://example.org/a",
|
||||
visited_at: "2026-09-27T12:00:00Z",
|
||||
passages: ["p-1", "p-2"],
|
||||
claims_supported: 2,
|
||||
health: "live",
|
||||
authority_tier: "official",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
const client = new GeneratedApiClient(stubRoutes(routes));
|
||||
const ledger = await client.getLedger("abcd2345");
|
||||
expect(ledger.runId).toBe("abcd2345");
|
||||
expect(ledger.citations[0]?.verification).toBe("live");
|
||||
expect(ledger.citations[0]?.authorityTier).toBe(1);
|
||||
});
|
||||
|
||||
it("fetches the report markdown", async () => {
|
||||
const routes = {
|
||||
"http://127.0.0.1:8000/api/runs/abcd2345/report": () => ({
|
||||
status: 200,
|
||||
body: { run_id: "abcd2345", markdown: "# Report\n\nClaim [p-1]." },
|
||||
}),
|
||||
};
|
||||
const client = new GeneratedApiClient(stubRoutes(routes));
|
||||
const report = await client.getReport("abcd2345");
|
||||
expect(report.sections[0]?.body).toContain("[p-1]");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,390 @@
|
||||
/**
|
||||
* GeneratedApiClient — the live-mode ApiClient binding (v0.2 P3, REQ-26/REQ-54).
|
||||
*
|
||||
* Core research loop methods (methodologies, runs, events, report,
|
||||
* ledger, export) hit the FastAPI backend through openapi-fetch,
|
||||
* typed by the generated schema. Per G-002, the remaining prototype
|
||||
* surfaces (library, settings, suggestions, refine, graph, inbox,
|
||||
* profile) stay mock-backed until v0.3+ — this class delegates those
|
||||
* to the MockApiClient rather than reimplementing them.
|
||||
*
|
||||
* Components never import this class — only client.ts (the boundary)
|
||||
* does, selected by RESEARCHX_API_MODE=live (mock is the default).
|
||||
*/
|
||||
|
||||
import createClient from "openapi-fetch";
|
||||
|
||||
import type { paths } from "@/lib/api/generated/schema";
|
||||
import type {
|
||||
CitationLedger,
|
||||
Methodology,
|
||||
Report,
|
||||
ResearchRun,
|
||||
TraceEvent,
|
||||
} from "@/lib/types";
|
||||
import type {
|
||||
CreateRunInput,
|
||||
RunEventSubscription,
|
||||
} from "@/lib/api/client";
|
||||
import { MockApiClient } from "@/lib/api/mock/mock-client";
|
||||
|
||||
function baseUrl(): string {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://127.0.0.1:8000"
|
||||
);
|
||||
}
|
||||
|
||||
type BackendRun = {
|
||||
id: string;
|
||||
status: string;
|
||||
question: string;
|
||||
methodology: string;
|
||||
plan: {
|
||||
sub_questions: string[];
|
||||
source_universe: string[];
|
||||
outline: string[];
|
||||
} | null;
|
||||
started_at: string;
|
||||
};
|
||||
|
||||
type BackendCard = {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
eta_minutes: number;
|
||||
cost_estimate: string;
|
||||
why_recommended: string;
|
||||
output_shape: string;
|
||||
dials: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export class GeneratedApiClient extends MockApiClient {
|
||||
#client: ReturnType<typeof createClient<paths>>;
|
||||
|
||||
constructor(fetchImpl?: typeof globalThis.fetch) {
|
||||
super();
|
||||
this.#client = createClient<paths>({
|
||||
baseUrl: baseUrl(),
|
||||
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
override async getMethodologies(): Promise<Methodology[]> {
|
||||
const { data, error } = await this.#client.GET("/api/methodologies");
|
||||
if (error || !data) {
|
||||
throw new Error("live: failed to load methodologies");
|
||||
}
|
||||
return (data as BackendCard[]).map((card, index) => ({
|
||||
slug: card.slug,
|
||||
name: card.name,
|
||||
tagline: card.description.split(".")[0] ?? card.description,
|
||||
description: card.description,
|
||||
etaMinutes: card.eta_minutes,
|
||||
costEstimate: card.cost_estimate,
|
||||
whyRecommended: card.why_recommended,
|
||||
recommended: index === 0,
|
||||
verification: card.slug === "deep-report" ? "full" : "standard",
|
||||
dialSummary: Object.entries(card.dials).map(
|
||||
([dial, value]) => `${dial}: ${String(value)}`,
|
||||
),
|
||||
defaultDials: card.dials as unknown as Methodology["defaultDials"],
|
||||
badge: card.slug === "rapid-answer" ? "Fastest" : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
#toRun(run: BackendRun): ResearchRun {
|
||||
const plan = run.plan;
|
||||
return {
|
||||
id: run.id,
|
||||
status: run.status as ResearchRun["status"],
|
||||
plan: {
|
||||
id: run.id,
|
||||
question: run.question,
|
||||
methodologySlug: run.methodology,
|
||||
subQuestions: plan?.sub_questions ?? [],
|
||||
sourceUniverse: plan?.source_universe ?? [],
|
||||
outline: plan?.outline ?? [],
|
||||
},
|
||||
startedAt: run.started_at,
|
||||
};
|
||||
}
|
||||
|
||||
override async createRun(input: CreateRunInput): Promise<ResearchRun> {
|
||||
const overrides: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(input.dialOverrides ?? {})) {
|
||||
const snake = key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
||||
overrides[snake] = value;
|
||||
}
|
||||
const { data, error } = await this.#client.POST("/api/runs", {
|
||||
body: {
|
||||
question: input.question,
|
||||
context: input.context ?? "",
|
||||
methodology: input.methodologySlug,
|
||||
dial_overrides: overrides,
|
||||
},
|
||||
});
|
||||
if (error || !data) {
|
||||
throw new Error("live: failed to create run");
|
||||
}
|
||||
return this.#toRun(data as BackendRun);
|
||||
}
|
||||
|
||||
override async getRun(id: string): Promise<ResearchRun> {
|
||||
const { data, error } = await this.#client.GET("/api/runs/{run_id}", {
|
||||
params: { path: { run_id: id } },
|
||||
});
|
||||
if (error || !data) {
|
||||
throw new Error(`live: failed to load run ${id}`);
|
||||
}
|
||||
return this.#toRun(data as BackendRun);
|
||||
}
|
||||
|
||||
override async approvePlan(id: string): Promise<ResearchRun> {
|
||||
const { data, error } = await this.#client.POST(
|
||||
"/api/runs/{run_id}/approve",
|
||||
{ params: { path: { run_id: id } }, body: {} },
|
||||
);
|
||||
if (error || !data) {
|
||||
throw new Error(`live: failed to approve run ${id}`);
|
||||
}
|
||||
return this.#toRun(data as BackendRun);
|
||||
}
|
||||
|
||||
override async cancelRun(id: string): Promise<ResearchRun> {
|
||||
const { data, error } = await this.#client.POST(
|
||||
"/api/runs/{run_id}/cancel",
|
||||
{ params: { path: { run_id: id } } },
|
||||
);
|
||||
if (error || !data) {
|
||||
throw new Error(`live: failed to cancel run ${id}`);
|
||||
}
|
||||
return this.#toRun(data as BackendRun);
|
||||
}
|
||||
|
||||
async pauseRun(id: string): Promise<ResearchRun> {
|
||||
const { data, error } = await this.#client.POST(
|
||||
"/api/runs/{run_id}/pause",
|
||||
{ params: { path: { run_id: id } } },
|
||||
);
|
||||
if (error || !data) {
|
||||
throw new Error(`live: failed to pause run ${id}`);
|
||||
}
|
||||
return this.#toRun(data as BackendRun);
|
||||
}
|
||||
|
||||
async resumeRun(id: string): Promise<ResearchRun> {
|
||||
const { data, error } = await this.#client.POST(
|
||||
"/api/runs/{run_id}/resume",
|
||||
{ params: { path: { run_id: id } } },
|
||||
);
|
||||
if (error || !data) {
|
||||
throw new Error(`live: failed to resume run ${id}`);
|
||||
}
|
||||
return this.#toRun(data as BackendRun);
|
||||
}
|
||||
|
||||
override getRunEvents(
|
||||
id: string,
|
||||
onEvent: (event: TraceEvent) => void,
|
||||
onDone?: () => void,
|
||||
): RunEventSubscription {
|
||||
const source = new EventSource(
|
||||
`${baseUrl()}/api/runs/${encodeURIComponent(id)}/events`,
|
||||
);
|
||||
let paused = false;
|
||||
let buffer: TraceEvent[] = [];
|
||||
let counter = 0;
|
||||
|
||||
const deliver = (event: TraceEvent) => {
|
||||
if (paused) {
|
||||
buffer.push(event);
|
||||
} else {
|
||||
onEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
const mapType = (type: string): TraceEvent["type"] => {
|
||||
if (type === "query_fired") return "query";
|
||||
if (type === "source_visited") return "source_visited";
|
||||
if (type === "passage_extracted") return "passage_extracted";
|
||||
if (type === "state_changed") return "status";
|
||||
if (type === "run_done" || type === "run_started") return "status";
|
||||
if (type === "run_error") return "status";
|
||||
return "status";
|
||||
};
|
||||
|
||||
const handle = (backendEvent: { type: string; data: unknown }) => {
|
||||
let detail = "";
|
||||
let message = backendEvent.type;
|
||||
if (typeof backendEvent.data === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(backendEvent.data) as {
|
||||
type: string;
|
||||
data: Record<string, string>;
|
||||
};
|
||||
message =
|
||||
parsed.data?.message ??
|
||||
parsed.data?.query ??
|
||||
parsed.data?.url ??
|
||||
parsed.data?.state ??
|
||||
backendEvent.type;
|
||||
detail = JSON.stringify(parsed.data);
|
||||
} catch {
|
||||
detail = backendEvent.data;
|
||||
}
|
||||
}
|
||||
counter += 1;
|
||||
deliver({
|
||||
id: `live-${counter}`,
|
||||
ts: new Date().toISOString(),
|
||||
type: mapType(backendEvent.type),
|
||||
message,
|
||||
detail,
|
||||
});
|
||||
if (backendEvent.type === "run_done" || backendEvent.type === "run_error") {
|
||||
source.close();
|
||||
onDone?.();
|
||||
}
|
||||
};
|
||||
|
||||
source.onmessage = (raw) => {
|
||||
handle({ type: "message", data: raw.data });
|
||||
};
|
||||
for (const type of [
|
||||
"run_started",
|
||||
"query_fired",
|
||||
"source_found",
|
||||
"source_visited",
|
||||
"passage_extracted",
|
||||
"source_refused",
|
||||
"state_changed",
|
||||
"probe_attached",
|
||||
"run_done",
|
||||
"run_error",
|
||||
]) {
|
||||
source.addEventListener(type, (raw) => {
|
||||
handle({ type, data: (raw as MessageEvent).data });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cancel() {
|
||||
source.close();
|
||||
},
|
||||
pause() {
|
||||
paused = true;
|
||||
},
|
||||
resume() {
|
||||
paused = false;
|
||||
const pending = buffer;
|
||||
buffer = [];
|
||||
for (const event of pending) {
|
||||
onEvent(event);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async getReport(id: string): Promise<Report> {
|
||||
const { data, error } = await this.#client.GET(
|
||||
"/api/runs/{run_id}/report",
|
||||
{ params: { path: { run_id: id } } },
|
||||
);
|
||||
if (error || !data) {
|
||||
throw new Error(`live: report not ready for run ${id}`);
|
||||
}
|
||||
const markdown = (data as { run_id: string; markdown: string }).markdown;
|
||||
return {
|
||||
id,
|
||||
runId: id,
|
||||
title: "Research Report",
|
||||
sections: [{ heading: "Report", body: markdown }],
|
||||
citations: [],
|
||||
};
|
||||
}
|
||||
|
||||
override async getLedger(id: string): Promise<CitationLedger> {
|
||||
const { data, error } = await this.#client.GET(
|
||||
"/api/runs/{run_id}/ledger",
|
||||
{ params: { path: { run_id: id } } },
|
||||
);
|
||||
if (error || !data) {
|
||||
throw new Error(`live: ledger not ready for run ${id}`);
|
||||
}
|
||||
const backend = data as {
|
||||
run_id: string;
|
||||
generated_at: string;
|
||||
entries: Array<{
|
||||
url: string;
|
||||
visited_at: string;
|
||||
passages: string[];
|
||||
claims_supported: number;
|
||||
health: string;
|
||||
authority_tier: string;
|
||||
}>;
|
||||
};
|
||||
return {
|
||||
runId: backend.run_id,
|
||||
generatedAt: backend.generated_at,
|
||||
citations: backend.entries.map((entry, index) => ({
|
||||
id: `c-${index + 1}`,
|
||||
url: entry.url,
|
||||
title: entry.url,
|
||||
site: entry.url,
|
||||
passageId: entry.passages[0] ?? "",
|
||||
quote: "",
|
||||
verification: (entry.health === "live"
|
||||
? "live"
|
||||
: entry.health === "stale"
|
||||
? "stale"
|
||||
: "dead") as CitationLedger["citations"][number]["verification"],
|
||||
authorityTier: (entry.authority_tier === "official"
|
||||
? 1
|
||||
: entry.authority_tier === "reference"
|
||||
? 2
|
||||
: 3) as CitationLedger["citations"][number]["authorityTier"],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
override async exportReport(
|
||||
id: string,
|
||||
format: "markdown" | "json",
|
||||
): Promise<{ filename: string; content: string }> {
|
||||
const [report, ledger] = await Promise.all([this.getReport(id), this.getLedger(id)]);
|
||||
if (format === "markdown") {
|
||||
const body = report.sections.map((s) => `## ${s.heading}\n\n${s.body}`).join("\n\n");
|
||||
const citationsBlock = ledger.citations
|
||||
.map(
|
||||
(c, i) =>
|
||||
`${i + 1}. [${c.url}](${c.url}) — health: ${c.verification}, tier: ${c.authorityTier}`,
|
||||
)
|
||||
.join("\n");
|
||||
return {
|
||||
filename: `researchx-${id}.md`,
|
||||
content:
|
||||
`# Research Report\n\n${body}\n\n## Citations\n\n${citationsBlock || "(no citations)"}\n`,
|
||||
};
|
||||
}
|
||||
const payload = {
|
||||
run_id: id,
|
||||
generated_at: ledger.generatedAt,
|
||||
entries: ledger.citations.map((c) => ({
|
||||
url: c.url,
|
||||
visited_at: ledger.generatedAt,
|
||||
passages: c.passageId ? [c.passageId] : [],
|
||||
claims_supported: 1,
|
||||
health: c.verification,
|
||||
authority_tier: c.authorityTier === 1 ? "official" : c.authorityTier === 2 ? "reference" : "general",
|
||||
})),
|
||||
checklist: {
|
||||
run_id: id,
|
||||
citations_checked: ledger.citations.length,
|
||||
},
|
||||
};
|
||||
return {
|
||||
filename: `researchx-${id}.json`,
|
||||
content: JSON.stringify(payload, null, 2),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Shared test fixtures for live-binding contract tests. */
|
||||
|
||||
export interface BackendRun {
|
||||
id: string;
|
||||
status: string;
|
||||
question: string;
|
||||
methodology: string;
|
||||
plan: {
|
||||
sub_questions: string[];
|
||||
source_universe: string[];
|
||||
outline: string[];
|
||||
} | null;
|
||||
started_at: string;
|
||||
}
|
||||
Reference in New Issue
Block a user