diff --git a/.ladle/components.tsx b/.ladle/components.tsx new file mode 100644 index 00000000..469c21ac --- /dev/null +++ b/.ladle/components.tsx @@ -0,0 +1,13 @@ +import type { GlobalProvider } from '@ladle/react'; + +import '../src/client/index.css'; + +const THEME_KEY = 'brunch-ladle-theme'; + +export const Provider: GlobalProvider = ({ children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/.ladle/theme.css b/.ladle/theme.css new file mode 100644 index 00000000..d5573a37 --- /dev/null +++ b/.ladle/theme.css @@ -0,0 +1,5 @@ +@import 'tailwindcss'; +@import '../src/client/index.css'; + +@source "../src/client/components/**/*.tsx"; +@source "../src/client/**/*.stories.tsx"; diff --git a/.mcp.json b/.mcp.json index 5a699e20..9d30c317 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,13 +2,7 @@ "mcpServers": { "shadcn": { "command": "npx", - "args": [ - "shadcn@latest", - "mcp" - ] - }, - "forge_extension": { - "url": "http://localhost:58459/mcp" + "args": ["shadcn@latest", "mcp"] } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 131768d3..a52c1288 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,54 @@ Open http://localhost:5173. | `npm run test` | Run test suite (vitest) | | `npm run verify` | Full gate: lint + format + test + build | | `npm run fix` | Auto-fix lint + format issues | +| `npm run seed ` | Seed a database with a named fixture scenario | + +## Fixture scenarios + +Seed the dev database with pre-built project states for testing and development: + +```bash +# List available scenarios +npm run seed + +# Seed into the default brunch.db (what npm run dev reads) +npm run seed issue-tracker-all-phases-closed + +# Seed into a specific file +npm run seed issue-tracker-scope-closed ./tmp/test.db + +# Wipe and re-seed +rm brunch.db brunch.db-shm brunch.db-wal +npm run seed issue-tracker-all-phases-closed +``` + +**Programmatic scenarios** — skeleton fixtures with minimal turns, no realistic parts: + +| Scenario | State | +|---|---| +| `scope-closed` | Scope phase closed, design not started | +| `design-active` | Scope closed, one design turn | +| `requirements-ready` | Scope + design closed, requirements reviewed | +| `criteria-ready` | + requirements closed, criteria reviewed | +| `all-phases-closed` | All four phases closed | +| `forced-close-all-phases-closed` | All four phases closed, with design closed via user-forced closure | +| `low-readiness-all-phases-closed` | All four phases closed, with a synthetic low-readiness scope closure for export-caveat testing | + +**Manifest scenarios** — rich fixtures with realistic interview content, structured parts, knowledge items, and cross-kind edges (domain: tiny issue tracker): + +| Scenario | State | Items | Edges | +|---|---|---|---| +| `issue-tracker-scope-closed` | Scope closed (5 turns + proposal/confirm) | 12 (goals, terms, contexts, constraints) | 3 | +| `issue-tracker-design-active` | + 2 design turns | 18 (+ decisions, assumptions) | 7 | +| `issue-tracker-requirements-ready` | + design closed, requirements reviewed | 23 (+ 5 requirements, mixed review) | 10 | +| `issue-tracker-criteria-ready` | + requirements closed, criteria reviewed | 27 (+ 4 criteria, mixed review) | 14 | +| `issue-tracker-all-phases-closed` | All phases closed | 27 | 14 | + +### Source tracing + +- **Programmatic**: `src/server/fixtures/scenarios.ts` — inline seed functions +- **Manifest**: `src/server/fixtures/manifests/issue-tracker.json` — static JSON content; `src/server/fixtures/manifest.ts` — seeder that wires manifests through DB functions +- Naming convention: `issue-tracker-*` scenarios come from the manifest; unprefixed ones are programmatic ## Architecture @@ -88,15 +136,13 @@ src/ ## Current state -**Working**: Scope-phase interview with structured questions, observer entity extraction, entity sidebar, conversation persistence and resume, project management. - -**Known issue**: Structured turn card does not render during live streaming — appears only after page refresh. Server persists correctly; hydration from DB works. Fix is next on the critical path (see `memory/PLAN.md` slice 6c). +**Working**: Full four-phase interview (scope → design → requirements → criteria), phase-aware observer extraction across all 8 canonical knowledge kinds, explicit phase outcomes with closure provenance, requirements and criteria review with approve/reject state, knowledge workspace, markdown export, project dashboard with workflow state, fixture scenarios with rich seeded content. -**Not yet built**: Phase transitions (7), design/requirements/criteria phases (8-10), decision revisit/branching (11), entity lifecycle API (12), spec export (13), npx distribution (14). See `memory/PLAN.md` for the full roadmap. +**Not yet built**: Review lifecycle refinement (13a), npx distribution (14). See `memory/PLAN.md` for the full roadmap. ## Tests -67 tests across 7 test files covering DB operations, app routes, core logic, interview flow, observer extraction, parts serialization, and context builders. Provider calls are mocked for CI; prompt quality depends on manual evaluation. +223 tests across 24 test files covering DB operations, app routes, core logic, interview flow, observer extraction, parts serialization, context builders, workspace hydration/controller/data, client components, phase-close logic, and build boundaries. Provider calls are mocked for CI; prompt quality depends on manual evaluation. ```bash npm test diff --git a/docs/design/BROWNFIELD_EXPLORATION.md b/docs/design/BROWNFIELD_EXPLORATION.md new file mode 100644 index 00000000..c18707ad --- /dev/null +++ b/docs/design/BROWNFIELD_EXPLORATION.md @@ -0,0 +1,75 @@ +# Brownfield Exploration Design + +> Design exploration from 2026-04-12. Referenced by SPEC.md D82, D83. +> Status: **approved direction** — First-turn exploration via prompt + core tools. + +## Shape + +No new module boundary. Brownfield exploration is a prompt/context/tool-configuration concern: + +1. **Tool set:** In brownfield mode, the interviewer agent receives core tools (read, grep, find, ls, bash) alongside interview tools (ask_question, propose_phase_closure). +2. **System prompt:** A brownfield variant of the scope system prompt instructs the agent to explore the codebase before asking its first scope question. +3. **Context builder:** `buildInterviewerContext()` receives the project's `cwd` and `mode` (greenfield/brownfield) to construct the appropriate first-turn prompt. + +## Interviewer configuration change + +```typescript +// interview.ts — createInterviewerAgent() +const tools = { + ask_question, + ...(closeable ? { propose_phase_closure } : {}), + ...(mode === 'brownfield' ? createCoreTools(projectCwd) : {}), +} + +const instructions = mode === 'brownfield' + ? getBrownfieldSystemPrompt(phase) + : getSystemPrompt(phase) +``` + +## First-turn UX + +The user sees one continuous streamed turn: +1. Agent uses tool calls (read_file, grep, find_files) to explore the codebase +2. Agent synthesizes findings into a summary +3. Agent transitions into the first scope question, grounded in what it found +4. Observer extracts knowledge items from this turn as usual + +## Data model change + +The `project` table needs a `mode` field: +```typescript +mode: text('mode', { enum: ['greenfield', 'brownfield'] }).notNull().default('greenfield') +``` + +The project's `cwd` is stored alongside it (needed for tool factory): +```typescript +cwd: text('cwd') // absolute path where the project was created +``` + +## First-screen routing + +The client's project creation flow asks: +- "New concept from scratch" → greenfield +- "Feature within existing codebase" → brownfield + +Both create a project record; brownfield sets `mode: 'brownfield'` and stores `cwd`. + +## Design alternatives considered + +- **B (Separate pre-interview agent call):** Dedicated exploration agent runs before the first chat turn. Clean separation but adds latency (two serial agent calls) and the exploration is non-interactive — user can't guide it or interrupt. +- **A (Conditional tool set only):** Just add tools, no special prompt. Relies entirely on the agent figuring out it should explore. Too unpredictable. + +## Prompt engineering notes + +The brownfield system prompt should: +- Name the project directory explicitly +- Instruct: "Before asking your first scope question, use your tools to explore the codebase" +- Suggest a strategy: look for README, package.json/Cargo.toml/etc., directory structure, then key files +- Set a budget: "Spend no more than 5-8 tool calls on exploration before synthesizing" +- Transition: "Once you have a working understanding, summarize what you found and begin scope questions grounded in that context" + +## Open questions + +- Should brownfield mode persist core tool access throughout the entire interview, or only on the first turn? (Probably throughout — the user might say "look at the auth module" during design phase.) +- Should the exploration summary be stored as a special data part for context rebuilding? (Probably not for V1 — it's just part of the first turn's assistant_parts.) +- Does the `ToolLoopAgent` `stopWhen: stepCountIs(4)` need to increase for brownfield first turns? (Likely yes — exploration needs more steps.) diff --git a/docs/design/LOCAL_STORAGE.md b/docs/design/LOCAL_STORAGE.md new file mode 100644 index 00000000..136c246b --- /dev/null +++ b/docs/design/LOCAL_STORAGE.md @@ -0,0 +1,63 @@ +# Local-First Storage Design + +> Design exploration from 2026-04-12. Referenced by SPEC.md D81. +> Status: **approved direction** — BrunchProject struct with shallow walk-up. + +## Shape + +```typescript +interface BrunchProject { + root: string // absolute path to .brunch/ + dbPath: string // .brunch/brunch.db + configPath: string // .brunch/config.json (future) + cwd: string // the directory where .brunch/ was found or created +} + +/** Walk up from cwd looking for .brunch/ (max ~5 levels, stop at home or root). + * Returns null if not found. */ +function findBrunchProject(cwd: string): BrunchProject | null + +/** Create .brunch/ in cwd. Throws if it already exists. */ +function initBrunchProject(cwd: string): BrunchProject + +/** Find or create: find first, create if not found. */ +function resolveBrunchProject(cwd: string): BrunchProject +``` + +## Walk-up behavior + +Matches `.git` discovery semantics: +1. Check `cwd/.brunch/` — if found, use it +2. Walk up parent directories (max 5 levels) +3. Stop at filesystem root or user home directory +4. If not found anywhere, create `.brunch/` in the original `cwd` + +## Integration with launcher + +```typescript +// src/server/index.ts (or future bin entry) +const project = resolveBrunchProject(process.cwd()) +const db = createDb(project.dbPath) +const app = createApp(db, project) +``` + +## .brunch/ directory structure + +``` +.brunch/ + brunch.db # SQLite database (all projects/runs within this directory) + brunch.db-wal # WAL file (auto-created by SQLite) + brunch.db-shm # shared memory file (auto-created by SQLite) + config.json # future: user preferences, model config +``` + +## Design alternatives considered + +- **A (Minimal):** Just change the default DB path to `.brunch/brunch.db`. No resolution logic, no struct. Too simple — doesn't handle subdirectory invocation or future config. +- **C (Explicit, no walk-up):** Only look in cwd. Simplest mental model but breaks when user runs from a subdirectory. + +## Open questions + +- Should `.brunch/` be added to common `.gitignore` templates? (Probably yes — DB contains API responses.) +- Should the walk-up depth be configurable? (Probably not — 5 levels is sufficient.) +- Should `resolveBrunchProject` print a message when it creates a new `.brunch/`? (Yes — first-run feedback.) diff --git a/docs/design/REVISIT_MODULE.md b/docs/design/REVISIT_MODULE.md new file mode 100644 index 00000000..4b00cc7c --- /dev/null +++ b/docs/design/REVISIT_MODULE.md @@ -0,0 +1,87 @@ +# Knowledge-Graph Revisit Module Design + +> Design exploration from 2026-04-12. Referenced by SPEC.md D80, D84. +> Status: **approved direction** — C-flavored hybrid (state machine projected from DB). + +## Shape + +State machine lifecycle reconstructed from DB state on each HTTP request. No in-memory state survives between requests. + +### Persisted state (new `revisit_session` table) + +```typescript +interface RevisitSession { + id: number + projectId: number + status: 'planned' | 'active' | 'closing' | 'done' | 'aborted' + rootItemIds: number[] // items the user invalidated + affectedItemIds: number[] // cascade result + phasesToReopen: Phase[] // derived from affected items + anchorTurnId: number // highest primary-tree turn linked to affected items + threadRootTurnId: number | null // set when thread opens + createdAt: string + completedAt: string | null +} +``` + +### Projected state (read-only, reconstructed per request) + +```typescript +type RevisitState = + | { status: 'none' } + | { status: 'planned'; session: RevisitSession; preview: CascadePreview } + | { status: 'active'; session: RevisitSession; resolved: number[]; remaining: number[] } + | { status: 'closing'; session: RevisitSession } + | { status: 'done'; session: RevisitSession } +``` + +### Module boundary (5 functions) + +```typescript +/** Read-only: compute cascade without writing anything */ +function previewCascade(projectId: number, itemIds: number[]): CascadePreview + +/** planned: write the session, mark items invalidated */ +function beginRevisit(projectId: number, itemIds: number[]): RevisitSession + +/** active: open the secondary thread, reopen phases */ +function openRevisitThread(sessionId: number, anchorTurnId: number): RevisitSession + +/** active: mark one item resolved (called per-item as conversation progresses) */ +function resolveRevisitItem(sessionId: number, itemId: number, outcome: 'confirmed' | 'edited' | 'removed'): RevisitState + +/** closing → done: finalize when all items resolved */ +function completeRevisit(sessionId: number): RevisitSession +``` + +## What it hides + +- Graph traversal (BFS over all edge types, cycle detection) +- Which phases to reopen (derived from affected items' kind → phase mapping) +- Phase outcome supersession writes +- Review state reset for affected items +- Secondary thread turn creation with correct phase/ancestry +- Resolution completeness checking +- Anchor turn calculation (highest primary-turn provenance among affected items) + +## HTTP mapping + +| Step | Trigger | Writes | +|---|---|---| +| `previewCascade` | User selects items in edit mode | None (read-only) | +| `beginRevisit` | User confirms cascade | `revisit_session` + `turnKnowledgeItem(invalidated)` | +| `openRevisitThread` | Immediately after begin | Thread root turn + phase outcome supersession | +| `resolveRevisitItem` | Chat handler after each secondary turn | `turnKnowledgeItem(confirmed/edited)` per item | +| `completeRevisit` | When `remaining` reaches zero | Session status → done | + +## Design alternatives considered + +- **A (Minimal):** 2-method surface (`preview` + `invalidate`). Too coarse — doesn't model the interactive conversation lifecycle that spans many HTTP requests. +- **B (Event-driven):** Discrete event pipeline. Good auditability but ordering enforcement is runtime-only, not compile-time. Implementation style adopted (each step is stateless, state from DB). +- **C (Pure state machine):** In-memory stateful machine. Doesn't fit HTTP-per-request model without external persistence. Shape adopted, implementation adapted to DB-projected state. + +## Open questions + +- How does the secondary thread's chat endpoint differ from the primary? Same `/api/projects/:id/chat` with a `threadId` param, or separate route? +- Does `resolveRevisitItem` happen automatically when the observer processes a secondary-thread turn, or does it require explicit user action? +- What happens if the user closes the browser mid-revisit? The session stays `active` in DB — next launch should resume. diff --git a/memory/PLAN.md b/memory/PLAN.md index 585c3b7f..85325f23 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -52,169 +52,83 @@ ### Slices 6c. **Live streaming fix** `done` - - Requirements: → SPEC.md §Requirements #2, #3, #4 - - Assumptions: → SPEC.md §Assumptions A16, A28 - - Invariants to respect: → SPEC.md §Invariants I16, I17, I18, I22 - - Invariants established: → SPEC.md §Invariants I24 - - Acceptance: streamed turn card appears live without refresh; `npm run verify` passes - - Result: workspace controller projects streamed `tool-ask_question` into visible turn card before durable route refresh - - Evidence: InterviewWorkspace.test.tsx, workspace-controller.test.tsx, workspace-data.test.ts, app.test.ts + - Shipped: workspace controller projects streamed `tool-ask_question` into visible turn card before durable route refresh + - Established: I24 (workspace seam) 6d. **Flexible turn-response model** `done` - - Requirements: → SPEC.md §Requirements #3, #6 - - Assumptions: → SPEC.md §Assumptions A16, A28, A33 - - Decisions: → SPEC.md §Decisions D23, D24, D25 - - Invariants to respect: → SPEC.md §Invariants I17, I18, I19, I22 - - Invariants established: → SPEC.md §Invariants I44 - - Acceptance: zero/one/many selections plus free-text round-trip through persistence, hydration, and interviewer context - - Result: `data-turn-response` parts carry structured replies; workspace stages multi-select locally and submits one response - - Tracer bullets: 6d.1 single + free-text `done`, 6d.2 free-text-only `done`, 6d.3 many-selection `done` + - Shipped: `data-turn-response` parts carry zero/one/many selections + free-text; workspace stages multi-select locally + - Established: I44 (turn response seam) 6e. **Generic knowledge layer schema + sidebar projection** `done` - - Requirements: → SPEC.md §Requirements #5, #6, #14 - - Assumptions: → SPEC.md §Assumptions A14 - - Decisions: → SPEC.md §Decisions D5, D13, D25, D49, D50, D51 - - Invariants to respect: → SPEC.md §Invariants I20, I21, I23 - - Invariants established: → SPEC.md §Invariants I48 - - Acceptance: generic knowledge items and edges load and display from the active path without losing resume behavior - - Result: `knowledge_item` + `turn_knowledge_item` + `knowledge_edge` persistence; entities API projects kind-specific collections plus typed relationships - - Tracer bullets: 6e.1 framing items `done`, 6e.2a legacy edges `done`, 6e.2b remaining kinds `done` + - Shipped: `knowledge_item` + `turn_knowledge_item` + `knowledge_edge` persistence; kind-specific entity collections + typed relationships + - Established: I48 (generic knowledge seam) 6f. **Phase-aware observer extraction** `done` - - Requirements: → SPEC.md §Requirements #5, #6, #11, #12 - - Assumptions: → SPEC.md §Assumptions A14, A20 - - Decisions: → SPEC.md §Decisions D4, D5, D13, D25 - - Invariants to respect: → SPEC.md §Invariants I20, I21, I23 - - Invariants established: → SPEC.md §Invariants I54 - - Acceptance: observer biases extraction by phase; results stream in-band to sidebar without breaking sync - - Result: scope yields goals/terms/contexts/constraints, design yields decisions/assumptions with scope spillover, requirements yields requirements, criteria yields criteria - - Tracer bullets: 6f.1 scope framing `done`, 6f.2 scope constraints `done`, 6f.3 design bias `done`, 6f.4a requirements `done`, 6f.4b criteria `done` - -## Phase 5: Mode Closure + Full Interview - - + - Shipped: observer biases extraction by phase — scope→goals/terms/contexts/constraints, design→decisions/assumptions, requirements→requirements, criteria→criteria + - Established: I54 (observer widening seam) + +## Phase 5: Mode Closure + Full Interview `done` ### Slices 7. **Explicit phase outcomes + scope closure** `done` - - Requirements: → SPEC.md §Requirements #7, #8 - - Assumptions: → SPEC.md §Assumptions A15, A28 - - Decisions: → SPEC.md §Decisions D2, D3, D6, D62, D65, D66 - - Invariants to respect: → SPEC.md §Invariants I18, I24 - - Invariants established: → SPEC.md §Invariants I72 - - Acceptance: scope proposes closure, user confirms, explicit phase outcome persists, workflow state updates - - Result: durable `phase_outcome` proposal/confirmation records; `data-phase-summary` + `data-confirmation` chat seams; workspace header shows scope status and confirmation card + - Shipped: durable `phase_outcome` records; `data-phase-summary` + `data-confirmation` chat seams; workspace header phase status + - Established: I72 (phase-close seam) - Debt: shared closeability/readiness/closure-basis generalization folded into slice 8 7a. **Knowledge-layer redesign spike** `done` - - Decisions: → SPEC.md §Decisions D5, D17, D59, D61, D62, D63, D64, D67, D68, D69 - - Invariants to respect: → SPEC.md §Invariants I20, I21, I23, I48 - - Acceptance: approved target model for canonical kinds, cross-kind edges, storage direction, and knowledge-workspace boundaries - - Result: canonical ontology is 8 kinds (`goal`, `term`, `context`, `constraint`, `assumption`, `decision`, `requirement`, `criterion`); `framing` demoted to migration alias; primary review UX is dedicated knowledge workspace, not sidebar + - Shipped: canonical ontology (8 kinds); `framing` demoted to migration alias; primary review UX is dedicated knowledge workspace 7b. **Canonical knowledge model foundation + cutover seam** `done` - - Assumptions: → SPEC.md §Assumptions A14, A40 - - Decisions: → SPEC.md §Decisions D5, D13, D17, D49, D51, D59, D61, D62, D63, D67, D68, D69 - - Invariants to respect: → SPEC.md §Invariants I20, I21, I23, I48, I72 - - Invariants established: → SPEC.md §Invariants I48, I54 - - Acceptance: all eight canonical kinds plus generic edges work; scope closure reads coherent scope bundle; no new writes rely on `framing` - - Result: registry/observer/entities/sidebar use canonical kinds on clean DB; decisions/assumptions persist through generic seam; compatibility projections preserve slice-7 readiness - - Tracer bullets: 7b.1 canonical scope kinds `done`, 7b.2 generic edge/storage cutover `done` + - Shipped: registry/observer/entities/sidebar use canonical kinds; decisions/assumptions persist through generic seam + - Established: I48 (updated), I54 (updated) 8. **Design mode (commitment / exploration)** `done` - - Requirements: → SPEC.md §Requirements #2, #3, #5, #6, #7, #8 - - Assumptions: → SPEC.md §Assumptions A14, A15, A28, A40 - - Decisions: → SPEC.md §Decisions D2, D5, D6, D61, D62, D65, D66, D67, D68, D70, D71, D72, D73, D74, D75 - - Invariants to respect: → SPEC.md §Invariants I18, I19, I21, I22, I72 - - Invariants established: → SPEC.md §Invariants I72 - - Acceptance: design mode enters after scope close; design turns yield commitments on canonical knowledge seam; user can accept recommended close or force-close with persisted closure basis - - Result: shared workflow projection (status/closeability/readiness/closureBasis) replaces scope-only seam; explicit discriminated phase-close commands; force-close availability from shared policy; durable closure basis on `phase_outcome` - - Tracer bullets: 8.1 design entry + shared workflow `done`, 8.2 design closure + requirements handoff `done`, 8.3 user-forced close + carried debt `done` + - Shipped: shared workflow projection replaces scope-only seam; discriminated phase-close commands; force-close + durable closure basis + - Established: I72 (updated) 9. **Requirements-review mode** `done` - - Requirements: → SPEC.md §Requirements #6, #7, #8, #11, #13 - - Assumptions: → SPEC.md §Assumptions A15, A28, A40, A44, A45, A46 - - Decisions: → SPEC.md §Decisions D2, D5, D6, D61, D62, D65, D66, D67, D68, D69, D70, D71, D77, D78, D79 - - Invariants to respect: → SPEC.md §Invariants I18, I19, I21, I24 - - Invariants established: → SPEC.md §Invariants I87 - - Acceptance: requirement set synthesized from canonical knowledge; explicit approve/reject state; requirements closeability + closure proposal; criteria handoff on confirmation - - Result: interviewer grounded in requirement inventory; targeted approve/reject via review metadata + `turn_knowledge_item` links; closeability from full review coverage; shared phase-close seam reused for requirements → criteria handoff - - Tracer bullets: 9.1 inventory grounding `done`, 9.2 targeted approval `done`, 9.3 targeted rejection `done`, 9.4 closeability + proposal `done`, 9.5 closure + criteria handoff `done` + - Shipped: interviewer grounded in requirement inventory; targeted approve/reject; closeability from full review coverage; shared phase-close reused for requirements → criteria handoff + - Established: I87 (requirements-review seam) 10.1 **Criteria grounding + first synthesis/review loop** `done` - - Requirements: → SPEC.md §Requirements #6, #8, #12 - - Assumptions: → SPEC.md §Assumptions A28, A40 - - Decisions: → SPEC.md §Decisions D25, D55, D56, D71 - - Candidate invariant goals: the first criteria turn is grounded in approved requirements; criteria-mode interviewer/observer behavior stays criteria-shaped and can persist one initial criterion through the existing seam - - Invariants to respect: → SPEC.md §Invariants I18, I19, I21, I24, I95, I96 - - Acceptance: after requirements closes, the first criteria turn includes the approved requirement inventory, asks a criteria-shaped question rather than a generic follow-up, and one initial criterion can round-trip through observer/entity persistence without dropping out of criteria mode - - **Verification approach**: inner — criteria context/prompt seam tests plus criterion projection tests. Middle — round-trip oracle proving approved requirement inventory → criteria interviewer turn → criterion persistence/entities refresh. Outer — manual walkthrough judges whether the first criteria turn feels grounded in the reviewed requirement set. - -10.2 **Explicit criterion review state + minimal closeability** — Establish the first explicit per-criterion review seam and deterministic closeability rule in one slice rather than splitting approval, rejection, and closeability into separate tracer bullets. `done` - - Requirements: → SPEC.md §Requirements #7, #8, #12, #13 - - Assumptions: → SPEC.md §Assumptions A15, A28 - - Decisions: → SPEC.md §Decisions D24, D61, D65, D66, D70 - - Candidate invariant goals: criteria project explicit `approved` / `rejected` / `pending` review state; criteria becomes closeable only when every current criterion has explicit non-pending review state - - Invariants to respect: → SPEC.md §Invariants I18, I21, I24, I62, I63, I96 - - Acceptance: a targeted criteria-review turn can persist one explicit positive review action and one explicit non-positive review action, read-side projection resolves latest review state per criterion, and workflow marks criteria closeable only when no criterion remains `pending` - - **Verification approach**: inner — criterion review metadata/read-model/workflow-state tests. Middle — round-trip oracle proving explicit criterion review actions persist and project without drift, plus lifecycle oracle proving criteria stays `in_progress` until review coverage is complete. Outer — manual criteria review walkthrough judges whether the thin approve/reject semantics are legible enough to keep moving. - -10.3 **Criteria closure + completed workflow state** — Reuse the shared phase-close seam to close the final workflow phase and project a completed interview state once criteria review reaches the minimum bar. `done` - - Requirements: → SPEC.md §Requirements #7, #8, #13 - - Assumptions: → SPEC.md §Assumptions A15, A28 - - Decisions: → SPEC.md §Decisions D65, D66, D71 - - Candidate invariant goals: the terminal phase can propose and confirm closure through the shared seam; workflow can project all phases closed with no stale active interviewer phase - - Invariants to respect: → SPEC.md §Invariants I18, I24, I96 - - Acceptance: once criteria is closeable, the interviewer can propose criteria closure, user confirmation persists the final `phase_outcome`, and workflow projects all phases closed with no remaining active phase before export - - **Verification approach**: inner — phase-summary/confirmation/workflow-state tests. Middle — round-trip oracle proving criteria proposal → confirmation → confirmed final outcome → completed workflow projection. Outer — manual walkthrough judges whether final closure feels coherent before export/polish work. + - Shipped: criteria interviewer grounded in approved requirements; initial criterion round-trips through observer/entity persistence + - Established: I97 (criteria-review grounding) + +10.2 **Explicit criterion review state + minimal closeability** `done` + - Shipped: per-criterion approve/reject with latest-action-wins projection; closeability from full criterion review coverage + - Established: I98 (criteria-review seam) + +10.3 **Criteria closure + completed workflow state** `done` + - Shipped: shared phase-close seam closes criteria; all four phases project `closed` with no stale active phase + - Established: I99 (criteria closure) ## Phase 6: Readiness Surfaces + Export + then export from the reviewed knowledge layer. Knowledge-graph revisit is now Phase 8. --> ### Slices -11a. **Project dashboard workflow state** `FE-573` `done` — Surface durable workflow state on the project list so users can tell which projects are unstarted, in progress, closed with debt, invalidated, or export-ready without opening each workspace. - - Requirements: → SPEC.md §Requirements #7, #8, #11, #12, #13, #15 - - Assumptions: → SPEC.md §Assumptions A15, A28 - - Decisions: → SPEC.md §Decisions D3, D17, D65, D66, D70 - - Candidate invariant goals: project-list workflow state derives from durable phase outcomes, closeability/readiness projection, and review records, not ad hoc turn heuristics - - Invariants to respect: → SPEC.md §Invariants I24 - - Acceptance: the project list shows each project's per-phase status/readiness/closure-basis summary from persisted readiness artifacts plus live workflow projection, distinguishes forced-close or low-readiness debt from ordinary closed state, and updates correctly after refresh/resume - - **Verification approach**: inner — workflow-summary projection tests plus project-list route/component tests. Outer — manual multi-project walkthrough covering in-progress, forced-close debt, invalidated, and export-ready states. - -12a. **Knowledge workspace review surface** `FE-574` `done` — Read-only phase-oriented workspace at `/project/:id/knowledge` for inspecting canonical knowledge items grouped by kind, with review-status badges and relationship context. The sidebar remains a compact summary; this is the first dedicated review surface (D63, D69). Assumes the redesigned knowledge ontology/graph from 7a + 7b. - - Requirements: → SPEC.md §Requirements #6, #11, #12, #13 - - Assumptions: → SPEC.md §Assumptions A14, A40 - - Decisions: → SPEC.md §Decisions D5, D17, D61, D63, D67, D68, D69 - - Candidate invariant goals: knowledge workspace presents all canonical kinds with review state and relationship context from the existing entities API without lossy sidebar compression - - Invariants to respect: → SPEC.md §Invariants I23, I24 - - Acceptance: navigate to knowledge workspace, see kind-grouped items with review badges and dependency edges, navigate back to interview; no new mutations or edit actions in this slice - - **Verification approach**: inner — route/component tests with mock EntitiesData. Outer — manual walkthrough from seeded project. - -12b. **Spec export from the reviewed knowledge layer** `FE-574` `done` — Render markdown export from active-path reviewed knowledge items and explicit phase outcomes, including closure caveats when a mode was closed with low readiness or user-forced basis. Export is enabled only when all phases are closed. - - Requirements: → SPEC.md §Requirements #13 - - Assumptions: — - - Decisions: → SPEC.md §Decisions D5, D17, D26, D65, D66, D70 - - Candidate invariant goals: export reflects active-path reviewed knowledge only; readiness predicate gates export correctly; closure provenance survives into the final artifact when it changes how trustworthy the result is - - Invariants to respect: → SPEC.md §Invariants I18, I21 - - Acceptance: complete all modes, navigate to export, see markdown preview grouped by kind with closure caveats, download `.md` file; export blocked when any phase is not closed - - **Verification approach**: inner — export rendering + API route tests. Outer — manual export from seeded all-phases-closed project. - -11b. **Fixture scenarios + dev seed CLI** `done` — Extract the programmatic seed helpers from `app.test.ts` into a shared fixture module (`src/server/fixtures/scenarios.ts`) and add a CLI entry point (`src/server/fixtures/seed.ts`) so the dev server can be started at any named project state for outer-loop manual testing. - - Requirements: → SPEC.md §Requirements #14 (resume), §Verification Design (outer-loop fixture capture) - - Assumptions: → SPEC.md §Assumptions A28 - - Decisions: — - - Candidate invariant goals: fixture scenarios produce DB states identical to what the existing middle-loop tests verify; test files import from the shared module instead of owning inline seed helpers - - Invariants to respect: → SPEC.md §Invariants I5, I6, I72, I87, I98, I99 - - Acceptance: `npm run seed ` creates a named-scenario project in a fresh or specified DB; the dev server renders the expected workflow state and turn history from that seeded state; existing tests still pass after the extraction refactor - - **Verification approach**: inner — type checking confirms scenario functions share the same DB API contract. Middle — existing test suite passes after extraction (characterization). Outer — manual dev-server walkthrough from each seeded scenario. +11a. **Project dashboard workflow state** `FE-573` `done` + - Shipped: project list shows per-phase status/readiness/closure-basis from durable phase outcomes + live workflow projection + - Distinguishes forced-close debt from ordinary closed state + +12a. **Knowledge workspace review surface** `FE-574` `done` + - Shipped: read-only `/project/:id/knowledge` route with kind-grouped items, review badges, and dependency edges + - First dedicated review surface beyond sidebar + +12b. **Spec export from the reviewed knowledge layer** `FE-574` `done` + - Shipped: markdown export from active-path reviewed knowledge with closure caveats; gated on all phases closed + - Evidence: export.test.ts, KnowledgeWorkspace.test.tsx + +11b. **Fixture scenarios + dev seed CLI** `done` + - Shipped: shared fixture module + `npm run seed ` CLI; test files import from shared module + - 10 programmatic scenarios cover all workflow states + +11c. **Rich fixture generation for outer-loop testing** `done` + - Shipped: JSON manifest seeder with issue-tracker domain (5 scenarios, 27 knowledge items, 14 edges, 24 turns) + - Evidence: 223 tests pass; all 10 scenarios seed; knowledge workspace + export render from seeded state 13a. **Review lifecycle refinement across requirements + criteria** — Revisit the first-cut review model only after the thin end-to-end path is working, and add the deferred variants that were intentionally excluded from slices 9 and 10 so the app kept moving toward completion. Depends on 12a + 12b. `not-started` - Requirements: → SPEC.md §Requirements #11, #12, #13 @@ -225,26 +139,73 @@ - Acceptance: deferred review refinements such as edit/add/merge/stale semantics across requirements and criteria can land behind one cross-cutting slice without regressing completion, export, or workflow-state coherence - **Verification approach**: inner — mutation/read-model/invalidation tests per refinement added. Outer — manual cross-phase review lifecycle walkthrough after the dedicated knowledge workspace exists. -## Phase 7: Distribution +## Phase 7: Distribution + Brownfield + UI Alignment - + ### Slices -14. **npx distribution + CLI** — `bin` entry, launcher starts Express (serves built Vite assets + API on one port), opens browser. `npx brunch` for web UI. `npx brunch [command]` for CLI operations. Single env var: `ANTHROPIC_API_KEY`. `not-started` - - Requirements: → SPEC.md §Requirements #1 - - Decisions: → SPEC.md §Decisions D20 - - Candidate invariant goals: packaged launcher preserves working DB lifecycle and browser boot flow +17. **UI refinement + design-system alignment** — Port design tokens, layout primitives, card patterns, and component iteration environment from the parallel `brunch-ui` prototype. **Foundation**: Inter Variable font (replacing Geist), Figma-precise color ramp (ink/sub/hint/rule/wash/tint), shadow tokens (card/ring/card-ring), fine-grained typography scale (xxs 10px–sm 16px), font weight discipline (regular/medium/semi-bold only). **Layout shell**: AppHeader, StageSidebar/PhaseSidebar with working collapse/expand toggle, resizable main+right panels via `react-resizable-panels`, AppFooter spanning full width. **Components**: card-within-card pattern (white header / tinted body) for knowledge group cards and question cards; collapsible question card (V2 with inline `why` grounding, answered cards collapse to read-only summary, re-answering routes through revisit model not casual toggle); empty-state vocabulary (6 patterns: text-only, with icon, with CTA, centered hero, inline within list, attention/warning); metadata rows (label-over-value flex columns); badge refinement (mono font, no black-on-color, no all-caps); mandatory skeleton loading for all pending states. **Infrastructure**: Ladle story environment (`.ladle/` config with theme provider, `@source` directives, stories as living component reference), shadcn CLI + `components.json`. **Dependencies**: `@fontsource-variable/inter`, `react-resizable-panels`, `@ladle/react`, `shadcn`. `not-started` + - Requirements: → SPEC.md §Requirements #4, #5, #7, #9, #15 + - Decisions: → SPEC.md §Decisions D58, D59, D69 + - Candidate invariant goals: design tokens are Figma-authoritative and shared between app and Ladle stories; layout shell supports sidebar collapse/expand without breaking workspace data flow; card patterns are composable across knowledge workspace, interview workspace, and dashboard; empty-state and skeleton patterns are systematic, not ad-hoc; answered question cards visually communicate finality — re-opening to edit is a revisit-model action, not a UI toggle + - Invariants to respect: → SPEC.md §Invariants I24, I44, I48 + - Acceptance: Inter font renders across all routes; color ramp matches Figma reference values; StageSidebar collapses to PhaseSidebar and back; resizable panels work in interview and knowledge workspaces; at least one Ladle story renders with shared design tokens; question card shows inline `why` with collapsible answered state; empty states appear for all unpopulated sections; skeleton loading renders during route transitions + - **Verification approach**: inner — Ladle stories as visual regression surface; existing workspace seam tests pass after layout migration. Outer — manual walkthrough comparing rendered UI against Figma reference screens. + +14. **Local-first storage + npx distribution** — `resolveBrunchProject()` with shallow walk-up discovery creates/finds `.brunch/` directory. `bin` entry, Express launcher serves built Vite assets + API on one port, opens browser. `npx brunch` for web UI. Single env var: `ANTHROPIC_API_KEY`. `not-started` + - Requirements: → SPEC.md §Requirements #1, #14 + - Decisions: → SPEC.md §Decisions D10, D20, D81 + - Candidate invariant goals: `.brunch/` discovery works from subdirectories (walk-up); DB lifecycle unchanged after path migration; packaged launcher preserves working app - Invariants to respect: → SPEC.md §Invariants I1, I2, I4, I5 - - Acceptance: `npx brunch` with key in scope opens working app + - Acceptance: `npx brunch` in a project directory creates `.brunch/`, opens working app; running from subdirectory finds parent `.brunch/`; `BrunchProject` struct exposes root, dbPath, cwd + - **Verification approach**: inner — launcher/path-resolution tests plus packaged app smoke checks. Outer — packaged manual walkthrough includes seeded closed-project knowledge/export routes using `forced-close-all-phases-closed` and `low-readiness-all-phases-closed`, so the deferred Phase 6 browser coherence pass lands at the real distribution boundary rather than in the pre-distribution refactor thread. + - Design: `docs/design/LOCAL_STORAGE.md` + +14a. **Greenfield/brownfield first-screen + exploration** — First screen routes between greenfield (blank concept) and brownfield (existing codebase). Project records store `mode` and `cwd`. Brownfield adds core tools to interviewer, brownfield system prompt variant instructs explore-then-interview on first turn. Observer extracts from that turn as usual. `not-started` + - Requirements: → SPEC.md §Requirements #2, #3, #16 + - Assumptions: → SPEC.md §Assumptions A7, A47 + - Decisions: → SPEC.md §Decisions D32, D82, D83 + - Candidate invariant goals: brownfield first turn is grounded in discovered codebase context; greenfield path is unchanged; observer extracts from exploration turn normally + - Invariants to respect: → SPEC.md §Invariants I16, I22, I24, I54 + - Acceptance: create brownfield project → agent explores codebase → first scope question is grounded in findings; create greenfield project → existing scope flow unchanged + - Design: `docs/design/BROWNFIELD_EXPLORATION.md` + +## Phase 8: Knowledge-Graph Revisit (stretch) + + + +### Slices -## Phase 8: Post-Distribution Hardening +15. **Edit mode + cascade preview** — Knowledge workspace edit mode lets user select items to invalidate/remove. Read-only cascade preview traces knowledge graph edges (BFS over `depends_on`, `derived_from`, `constrains`, `verifies`, `refines`), shows affected items and phases that would reopen. No mutations yet — preview only. `not-started` + - Requirements: → SPEC.md §Requirements #10 + - Assumptions: → SPEC.md §Assumptions A48 + - Decisions: → SPEC.md §Decisions D5, D17, D80 + - Candidate invariant goals: cascade preview correctly identifies all downstream items via graph edges; preview matches what execution would produce; edit mode is modal (no other edits while active) + - Invariants to respect: → SPEC.md §Invariants I48 + - Acceptance: enter edit mode, select items, see accurate cascade preview with affected items and phases listed; exit without confirming leaves state unchanged + - Design: `docs/design/REVISIT_MODULE.md` + +15a. **Cascade execution + secondary thread lifecycle** — Confirm cascade → write invalidation records, create `revisit_session`, spawn secondary thread turn anchored to highest associated primary turn, reopen affected phases. Secondary thread conversation re-resolves affected items via interviewer. Complete revisit when all items resolved; phases can be re-closed. `not-started` + - Requirements: → SPEC.md §Requirements #10 + - Assumptions: → SPEC.md §Assumptions A48, A49 + - Decisions: → SPEC.md §Decisions D80, D84 + - Candidate invariant goals: secondary thread anchored to primary tree; affected phases reopen; re-resolution conversation produces valid knowledge items; session completes when all items resolved; knowledge item validity dual-checks active path + graph integrity + - Invariants to respect: → SPEC.md §Invariants I48, I54, I72, I87 + - Acceptance: confirm cascade → phases reopen → secondary thread conversation → all items resolved → phases can be re-closed → export valid again + - Design: `docs/design/REVISIT_MODULE.md` + +## Phase 9: Post-Distribution Hardening ### Slices -15. **Drizzle Kit audit remediation** — Revisit the current `npm audit` finding on `drizzle-kit` after distribution is stable. Do not use `npm audit fix --force`, which currently resolves to `drizzle-kit@0.18.1`; that downgrade crosses the modern config boundary and is not a safe path for this repo. Instead, validate a non-vulnerable upgrade path (currently the `1.0.0-beta` line) against this app's SQLite config, migration history, and `studio` workflow before changing dependencies. `not-started` +16. **Drizzle Kit audit remediation** — Revisit the current `npm audit` finding on `drizzle-kit` after distribution is stable. Do not use `npm audit fix --force`, which currently resolves to `drizzle-kit@0.18.1`; that downgrade crosses the modern config boundary and is not a safe path for this repo. Instead, validate a non-vulnerable upgrade path (currently the `1.0.0-beta` line) against this app's SQLite config, migration history, and `studio` workflow before changing dependencies. `not-started` - Requirements: → SPEC.md §Requirements #1 - Candidate invariant goals: packaged distribution remains stable while the Drizzle toolchain is upgraded off the vulnerable `@esbuild-kit/*` loader chain - Invariants to respect: → SPEC.md §Invariants I1, I2, I4, I5 @@ -253,17 +214,14 @@ ## Horizon - + -- Deferred from Phase 6: `11. Generalized revisit: branch + readiness invalidation` — revisit any earlier turn, branch from that point, restore the interview there, and invalidate downstream phase outcomes / review state from the affected frontier. Re-scope after the readiness/export path stabilizes. -- CLI interactive interview mode (terminal-based interview using core's DomainEvent stream) - MCP server adapter (expose core operations as MCP tools) -- Turn tree visualization (git-log-style branch graph in sidebar) -- Knowledge graph visualization (goal / term / context / constraint / assumption / decision / requirement / criterion view) -- Exploratory pathway (for projects where the goal itself is unclear) -- Project characterization kickoff mode (ToolLoopAgent with core tools explores existing codebase before interview) -- Multi-provider support via AI SDK provider abstraction (architecturally possible now) -- Export to GitHub Issues, Linear, YAML task definitions +- Knowledge graph visualization (interactive graph view of the canonical knowledge ontology) +- Exploratory pathway (for projects where the goal itself is unclear — distinct from brownfield which is about context, not goal uncertainty) +- Hard turn-tree branching (deferred from V1; the linked-list structure supports it but UX is not exposed) +- Git-integrated diff-able persistence format (file-based representation of the DB for version control) +- Headless interview driver (programmatic harness driving `/api/projects/:id/chat` with scripted answers) ## Dependencies @@ -271,41 +229,24 @@ ``` done ─────────────────────────────────────────────────────────────┐ - Phase 1: 1 (skeleton) ──→ 2 (SQLite) │ - Phase 2: 2 ──→ 3 ──→ 3c ──→ 3d │ - Phase 3: 3c ──→ 3b ──→ 4 ──→ 4a ──→ 4b ──→ 4c ──→ 5 ──→ 6 │ - spikes ──→ 6b (AI SDK pivot) │ + Phase 1–6: all complete │ ──────────────────────────────────────────────────────────────────┘ │ -Phase 4: 6b ──→ 6b1 (workspace oracle) ──→ 6c (live streaming fix) - 6c ──→ 6d (flexible turn-response model) - 6d ──→ 6e (generic knowledge layer) - 6e ──→ 6f (phase-aware observer) -Phase 5: 6f ──┬──→ 7 (explicit phase outcomes + scope closure) - └──→ 7a (knowledge-layer redesign spike) ──→ 7b (canonical knowledge foundation) - 7 ────┐ - 7b ───┴──→ 8 (design mode) ──→ 9 (requirements-review) ──→ 10.1 (criteria grounding) - 10.1 ──→ 10.2 (criterion review + closeability) - 10.2 ──→ 10.3 (criteria closure) -Phase 6: 7 ──┐ - 8 ──┼──→ 11a (project dashboard workflow state) - 9 ──┤ - 10.3 ─┘ - 10.3 ──→ 11b (fixture scenarios + dev seed CLI) - 7b ──→ 12a (knowledge workspace review surface) - 10.3 ──→ 12a - 10.3 ──→ 12b (export) - 12a ──┬──→ 13a (review lifecycle refinement) - 12b ──┘ -Phase 7: 12b ──→ 14 (npx + CLI) -Phase 8: 14 ──→ 15 (drizzle-kit audit remediation) +Phase 7: 12b ──→ 14 (local-first storage + npx distribution) + 14 ──→ 14a (greenfield/brownfield + exploration) + done ──→ 17 (UI refinement + design-system alignment) [parallel with 14] +Phase 8: 12a ──→ 15 (edit mode + cascade preview) [stretch] + 15 ──→ 15a (cascade execution + secondary threads) [stretch] +Phase 9: 14 ──→ 16 (drizzle-kit audit remediation) +Deferred: 12a + 12b ──→ 13a (review lifecycle refinement) ``` ### Parallelism opportunities -- 10.1–10.3 are done; the review seam has been unified (refactor landed between 10.3 and 11a). -- 11b (fixture scenarios + dev seed CLI) is done; seeded scenarios are available for outer-loop testing. -- 12a (knowledge workspace) and 12b (export) are unblocked and share one branch (FE-574). Build order: 12a → 12b. -- 13a (review lifecycle refinement) is explicitly deferred; it should collect rarer review variants after 12a and 12b stabilize rather than fragmenting slices 9 and 10. -- 14 (npx) can start early with a basic launcher, completing after slice 12b when the export predicate stabilizes. -- 15 (drizzle-kit audit remediation) should wait until 14 lands. +- Phase 6 is fully done (11a, 11b, 11c, 12a, 12b all complete). +- **14 (local-first + npx) and 17 (UI refinement) are both unblocked and can run in parallel.** +- 17 is purely presentational — no schema or API changes — so it has no dependency on 14 or vice versa. +- 14a (brownfield) depends on 14 landing first (needs the launcher and `.brunch/` resolution). +- 15 + 15a (knowledge-graph revisit) are stretch goals; they depend on 12a (knowledge workspace) which is done, but may not land before the first deadline. +- 13a (review lifecycle refinement) is explicitly deferred; it should collect rarer review variants after the revisit model stabilizes. +- 16 (drizzle-kit audit) should wait until 14 lands. diff --git a/memory/SPEC.md b/memory/SPEC.md index d9f342e5..501e9486 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -9,24 +9,27 @@ ## Concept & Goal -Brunch is an AI-guided spec elicitation tool that turns natural-language project goals into structured specifications through one branching interview that moves through four workflow modes: **scope** (goal / term / context / constraint discovery), **design** (commitment / exploration), **requirements** (audit / completeness), and **criteria** (verification). The interviewer agent leads the conversation with structured prompts, recommendations, and strategic grounding. A second observer agent extracts typed knowledge items from each turn and links them into a dependency graph. The output is a fire-and-forget specification document built from the active path. +Brunch is an AI-guided spec elicitation tool that turns natural-language project goals into structured specifications through a structured interview that moves through four workflow modes: **scope** (goal / term / context / constraint discovery), **design** (commitment / exploration), **requirements** (audit / completeness), and **criteria** (verification). The interviewer agent leads the conversation with structured prompts, recommendations, and strategic grounding. A second observer agent extracts typed knowledge items from each turn and links them into a dependency graph. The output is a fire-and-forget specification document built from the active path. + +Brunch supports both **greenfield** projects (new concept from scratch) and **brownfield** projects (new feature or sub-scope within an existing codebase). In brownfield mode, the interviewer agent explores the codebase using core tools before the first interview turn, grounding the conversation in discovered context. Storage is **local-first**: a `.brunch/` directory in the project folder (like `.git`), containing the SQLite database and any configuration. The core data model: -- **Turn tree** — The conversation is a tree of turns, not a flat log. Turns branch when an earlier turn is revisited. The active path from HEAD determines the current state. The turn tree *is* the version history — no snapshots needed. +- **Turn list** — The primary conversation is an ordered list of turns. Each turn points to its parent. `project.active_turn_id` is the HEAD pointer. The turn list is the primary interview record. Hard turn-tree branching is deferred; the current linked-list structure supports it structurally but branching/forking is not exposed in V1. +- **Secondary threads** — When the user revisits knowledge items (see Revisit model below), a modal secondary conversation thread is spawned, anchored to the highest implicitly associated turn in the primary conversation. Secondary threads carry their own turns and knowledge item associations, inheriting validity from the primary conversation through that anchor. - **Knowledge graph** — The target semantic layer is a typed cross-kind graph, not a set of disconnected sidebar buckets. Canonical durable knowledge kinds are: `goal`, `term`, `context`, `constraint`, `assumption`, `decision`, `requirement`, and `criterion`. These items can surface in any mode, link back to source turns, and connect through typed graph edges such as `depends_on`, `derived_from`, `constrains`, `verifies`, `motivates`, `defines`, and `refines`. The current `framing` label is a transitional Phase 4 umbrella, not the final canonical kind set. - **Capture-anytime / review-in-phase** — Modes are workflow controls, not exclusive capture windows. Any mode may surface any knowledge kind, but each mode owns the review/closure of the item families it is responsible for. -- **Readiness layer** — Export readiness is not just "the last turn felt done." Each mode produces an explicit phase outcome, and item families carry explicit review state. Revisit invalidates readiness from the affected turn frontier forward. +- **Readiness layer** — Export readiness is not just "the last turn felt done." Each mode produces an explicit phase outcome, and item families carry explicit review state. +- **Revisit model** — Revisit operates at the knowledge-graph level, not the turn-tree level. Users enter edit mode in the knowledge workspace, invalidate or remove knowledge items, and confirm the cascade. Affected downstream items (traced through knowledge graph edges) are flagged `needs-revisit`. A modal secondary conversation thread is spawned to walk through design-tree implications and re-resolve affected items, including downstream requirements and criteria. Affected phase outcomes reopen and must be re-closed before export. Knowledge item validity requires both: (1) linkage to a turn on the active path or in a secondary thread whose anchor is on the active path, and (2) no invalidated/removed parent items in the knowledge graph without resolution. The architecture (layered: db → core → adapters): +- **Storage**: Local-first `.brunch/` directory in the project folder (like `.git`). Contains the SQLite database and any configuration. Multiple elicitation runs / project versions coexist within one `.brunch/` directory, visible on the project dashboard. - **Database**: SQLite via Drizzle ORM + `better-sqlite3` — TypeScript schema is single source of truth for types, DDL, and migrations. Auto-applies at startup. - **Core**: Interface-agnostic service layer — turn tree operations, project-state loading, typed prompt/context building, knowledge-item lifecycle, observer invocation, phase management, readiness management, and export. No transport knowledge. -- **Agent engine**: AI SDK + Anthropic provider (`ai`, `@ai-sdk/anthropic`) — `ToolLoopAgent` powers the interviewer and `generateObject` powers the observer. Shared `BrunchUIMessage` / data-part contracts span request validation, persistence, server streaming, and client hydration. Future multi-step hardening builds on the AI SDK loop surface rather than a handwritten raw-event translator. (D30) +- **Agent engine**: AI SDK + Anthropic provider (`ai`, `@ai-sdk/anthropic`) — `ToolLoopAgent` powers the interviewer and `generateObject` powers the observer. The interviewer also has access to core filesystem tools (read, grep, find, ls, bash) for brownfield codebase exploration. Shared `BrunchUIMessage` / data-part contracts span request validation, persistence, server streaming, and client hydration. (D30) - **Observer agent**: Separate extraction call after each turn — captures typed knowledge items plus dependency / derivation edges using a phase-aware extraction policy. Invoked by core after turn completion. - **Web adapter**: Express.js returns AI SDK UI Message Stream SSE directly via `createUIMessageStream`. React + Vite + `@ai-sdk/react` `useChat` client consume the same typed message contract. -- **CLI adapter**: (future) Terminal I/O consuming the same `DomainEvent` stream -- **MCP adapter**: (future) MCP server exposing core operations as tools - **Output**: Flattened markdown spec exported on demand from the active path's reviewed knowledge items Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. @@ -34,9 +37,11 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. ## Constraints & Non-goals - **Anthropic-only** — no multi-provider support (OpenAI, Gemini, Ollama) -- **No automatic deletion cascading** — invalidation flags entities for review but does not delete or modify them. Two mechanisms: path exclusion (lazy, via HEAD movement) and flag propagation (eager, via dependency graph walk). See D17 +- **No automatic deletion cascading** — invalidation flags entities for review but does not delete or modify them. Cascade traces through knowledge graph edges; flagged items require explicit re-resolution through a secondary conversation thread. See D17, D80 - **No task planning** — consumers of the spec, not part of this tool -- **No exploratory pathway** — assumes user has a reasonably defined goal +- **No exploratory pathway** — assumes user has a reasonably defined goal. The greenfield/brownfield choice is about *context*, not goal clarity +- **No hard turn-tree branching for V1** — the linked-list turn structure supports future branching structurally, but fork/checkout UX is deferred. Revisit operates at the knowledge-graph level via edit mode + secondary threads +- **No explicit document ingestion UX for V1** — no @-mentions, drag-and-drop, or file picker for feeding documents to the interviewer. Brownfield context comes from agent codebase exploration + user verbal input - **Single-user** — no collaborative editing - **No custom model selection UI** — single model, configurable via env var at most - **No Dolt** — replaced by SQLite with turn-tree versioning @@ -46,21 +51,22 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. ## Requirements -1. Run `npx brunch` with just `ANTHROPIC_API_KEY` and have the tool open in the browser — setup is instant -2. Start a new project and have the agent begin a structured interview — scope questions establish goals, terms, context, and constraints before the design drill-down -3. Exploratory turns provide structured guidance (question, strategic grounding, options, recommendation when appropriate), but the user can answer with zero/one/many selections, rationale, and a custom answer when none of the options fit -4. See the AI's thinking process, tool usage, and progress in real-time — CLI-quality visibility of the agent's streaming output -5. The observer agent extracts typed knowledge items from each answered turn — `goal`, `term`, `context`, `constraint`, `assumption`, `decision`, `requirement`, `criterion` — plus dependency / derivation edges in the background -6. See the accumulated knowledge layer and readiness state in a dashboard as the interview progresses -7. Each workflow mode has its own deterministic closeability rule and a coarse readiness signal; the interviewer may recommend closing, and the user may close once the minimum bar is met -8. Phase / mode transitions surface status, readiness, and closure provenance clearly, and record a summary plus basis when the phase is closed -9. Revisit any previous turn by navigating the turn tree — this forks a new branch and soft-invalidates dependent downstream readiness for re-review -10. Abandon a revisit branch to return to the previous path — like git checkout +1. Run `npx brunch` in a project directory with just `ANTHROPIC_API_KEY` and have the tool open in the browser — setup is instant. State lives in a local `.brunch/` directory +2. On first launch, choose between **greenfield** (blank concept) and **brownfield** (existing codebase). Brownfield triggers agent codebase exploration before the first interview turn +3. Start a new project and have the agent begin a structured interview — scope questions establish goals, terms, context, and constraints before the design drill-down. In brownfield mode, the first turn is grounded in discovered codebase context +4. Exploratory turns provide structured guidance (question, strategic grounding, options, recommendation when appropriate), but the user can answer with zero/one/many selections, rationale, and a custom answer when none of the options fit +5. See the AI's thinking process, tool usage, and progress in real-time — CLI-quality visibility of the agent's streaming output +6. The observer agent extracts typed knowledge items from each answered turn — `goal`, `term`, `context`, `constraint`, `assumption`, `decision`, `requirement`, `criterion` — plus dependency / derivation edges in the background +7. See the accumulated knowledge layer and readiness state in a dashboard as the interview progresses +8. Each workflow mode has its own deterministic closeability rule and a coarse readiness signal; the interviewer may recommend closing, and the user may close once the minimum bar is met +9. Phase / mode transitions surface status, readiness, and closure provenance clearly, and record a summary plus basis when the phase is closed +10. Revisit knowledge items through edit mode in the knowledge workspace — invalidate or remove items, see cascade implications traced through knowledge graph edges, and confirm changes. A modal secondary conversation thread re-resolves affected downstream items and reopened phases 11. The requirements review mode gathers tentative requirements already surfaced, synthesizes a full requirement set from the knowledge layer, checks for gaps, and confirms completeness 12. The criteria review mode gathers tentative criteria already surfaced, synthesizes verification conditions from the knowledge layer plus approved requirements, and confirms coverage 13. Export the spec as markdown when all workflow modes are resolved, in-scope requirements and criteria are review-complete, and no upstream staleness remains on the active path 14. Close the browser and resume later — the turn tree, knowledge items, and readiness state persist in SQLite -15. The project dashboard shows all projects with their workflow state rather than only a binary completion flag +15. The project dashboard shows all elicitation runs/versions within the local `.brunch/` with their workflow state rather than only a binary completion flag +16. Partial-scope elicitation — the tool supports specifying a feature area or sub-scope within an existing project, not only greenfield whole-product specs ## Assumptions @@ -84,9 +90,9 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. Both were validated and are now embedded in D73, D75 and invariants I85, I86 rather than remaining live planning questions. Pruned 2026-04-09: removed A29, A41 — validated implementation-era cutover assumptions now - embedded in shipped architecture rather than live planning questions. A29 is embodied in the - shared core-tools boundary and horizon note; A41 is embodied in the completed 7b cutover and - no longer constrains future work. --> + embedded in shipped architecture rather than live planning questions. + Pruned 2026-04-12: removed A14, A21, A33, A45, A46 — all validated and now embedded as + structural properties of the codebase (I48/I54, I23/I24, I44, I87, I87 respectively). --> | # | Assumption | Confidence | Dependent decisions | Implicated slices | Validation approach | | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | --------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -94,67 +100,56 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. | A4 | Observer extraction completes in 1-3s during user read/think time (10-60s), adding zero perceived latency | medium | D1 | Observer agent | Spike measured 14-17s with Sonnet. Haiku expected 2-5s — validate with `generateObject` model switch. | | A6 | Turn-tree branching in SQLite is sufficient for decision revisit and undo in a single-user tool | high | D7 | Turn tree, Branching | Validate with realistic branch/merge scenarios | | A7 | Users arriving at the tool have a reasonably defined goal | medium | — | Scope phase | User testing; characterization kickoff mode mitigates if false | -| A14 | A second-thread observer agent can reliably extract typed knowledge items and graph edges from a turn plus accumulated context | **validated** | D4, D5, D13, D61, D62 | Observer agent, Knowledge layer | Validated: observer seam tests prove widened extraction for all eight canonical kinds through generic persistence, in-band sync, and workspace refresh. | | A15 | The LLM can produce a useful coarse readiness estimate and closure recommendation, but phase closure authority must not depend solely on that judgment | medium | D3, D65, D66, D70 | Phase resolution, readiness projection | Partially validated structurally: slices 8.2, 8.3, and 9.5 now prove the shared phase-closing seam supports interviewer-recommended closure across multiple phases plus user-forced design closure, with persisted closure basis surviving reload and handoff through requirements into criteria (`db.test.ts`, `core.test.ts`, `app.test.ts`, `InterviewWorkspace.test.tsx`). Remaining validation still depends on outer-loop comparison of model recommendations vs user overrides across varied project types. | | A16 | AI SDK `useChat` hook's `ToolUIPart` state machine models all permutations of pending, error, and success for tool calls | high | D14, D58 | Rich chat UI, pending-question projection | Partially validated: typed `tool-ask_question` parts render with correct state labels, streamed ask-question output now projects into a dedicated pending-question turn-card state without fabricating persisted turns in `workspace-data.test.ts`, `workspace-controller.test.tsx`, and `InterviewWorkspace.test.tsx`, and manual browser verification confirmed the pending-question card now appears without refresh. | | A20 | Observer results can be delivered as typed data parts on the existing chat stream without holding the connection open unacceptably long | high | D22 | Observer agent, Entity sidebar | Measure observer latency with `generateObject`; if >5s, fall back to out-of-band SSE | -| A21 | `useChat` `onData` callback reliably bridges to `queryClient.invalidateQueries` without stale-closure issues | **validated** | D22 | Entity sidebar | Validated: `InterviewWorkspace.test.tsx` proves observer-result → query invalidation → sidebar refresh. | -| A28 | AI SDK `ToolLoopAgent` with `stopWhen: stepCountIs(N)` is sufficient for brunch's multi-step interviewing, review, and phase-transition needs — no custom agent loop required | high | D30 | Agent loop, Phase transitions | Partially validated structurally: slices 8.1–8.3 plus 9.5 now prove confirmed scope closure can hand off into a design-phase interviewer/observer turn, that design can recommend closure and hand off into requirements, that a user-forced design close can bypass a recommendation without a handwritten loop, and that confirmed requirements closure hands the next `/chat` turn into `criteria` through the same shared seam (`interview.test.ts`, `db.test.ts`, `core.test.ts`, `app.test.ts`). Remaining proof for later review/closure slices still depends on the downstream criteria/export work. | -| A33 | Structured turn responses can replace today's single-select flow while keeping persisted response parts, transcript hydration, and downstream context projection aligned for the first thin slice | **validated** | D23, D24, D25, D57, D60 | 6d flexible turn-response model; Phase 4 response-seam refactor | Validated: zero/one/many selections plus free-text round-trip through persistence, hydration, and all downstream projections coherently. | +| A28 | AI SDK `ToolLoopAgent` with `stopWhen: stepCountIs(N)` is sufficient for brunch's multi-step interviewing, review, and phase-transition needs — no custom agent loop required | high | D30 | Agent loop, Phase transitions | Partially validated structurally: all four workflow modes (scope → design → requirements → criteria) prove phase-close handoff through the shared seam without a handwritten loop, including user-forced design close bypassing a recommendation. Full end-to-end validation (all modes closed → export) now complete. Remaining outer-loop judgment: whether ToolLoopAgent scaling is adequate for longer interviews (20+ turns). | | A40 | The observer and review workspace can discriminate `goal`, `term`, and `context` well enough for a first canonical-scope implementation if low-confidence cases stay reviewable instead of collapsing back into `framing` | medium | D5, D67, D68, D69 | 7b canonical knowledge model foundation; 8 design mode; 9 requirements-review mode; 12 knowledge workspace | Validate with fixture probes and the first canonical-scope review flow: measure confusion between `goal` / `term` / `context`, then confirm that workspace normalization/editing can correct low-confidence captures without losing provenance or blocking downstream review. | | A44 | The existing choice-turn response seam (multi-select plus free text) is sufficient for a first requirements-review interaction that asks whether the synthesized requirement set is complete, needs correction, or is missing items before dedicated per-item review actions exist | medium | D24, D25 | 9 requirements-review mode | Partially validated structurally: slice 9.1 now proves requirements-review grounding can include the current requirement inventory, that a missing-requirement reply can round-trip through the existing turn-response seam into observer/entity refresh, and that requirements remains `in_progress` / not yet closeable after the first review interaction (`context.test.ts`, `interview.test.ts`, `db.test.ts`, `app.test.ts`). Usability of the reused choice-turn UI remains an outer-loop judgment before richer per-item review actions land. | -| A45 | A first requirement-level review slice can establish durable explicit approval state for one requirement without yet changing requirements closeability or introducing the full edit/reject/stale lifecycle | **validated** | D61, D65, D70, D77 | 9 requirements-review mode | Validated: targeted approval persists durable `reviewed` turn/item links and projects `approved` vs `pending` state. | -| A46 | A first deterministic requirements closeability rule can treat every active-path requirement having explicit non-pending review state (`approved` or `rejected`) as sufficient for a closure proposal, while readiness remains a separate descriptive signal | **validated** | D65, D66, D70, D79 | 9 requirements-review mode; 10 criteria-review mode | Validated: full review coverage triggers closeability, shared phase-close seam reuses for requirements → criteria handoff. | +| A47 | Agent codebase exploration (using core tools: read, grep, find, ls, bash) produces sufficient context for the interviewer to ground a meaningful first brownfield interview turn without explicit document ingestion UX | medium | D32, D82, D83 | Brownfield kickoff, project characterization | Validate with manual brownfield walkthroughs across varied codebases (small/large, monorepo/single-package, documented/undocumented). | +| A48 | Knowledge graph edges are sufficient for tracing cascade implications when a knowledge item is invalidated or removed — the graph structure captures the meaningful dependency relationships that determine what needs re-resolution | medium | D5, D17, D80 | Knowledge-graph revisit, secondary threads | Validate structurally: cascade from invalidated assumption → affected decisions → affected requirements → affected criteria produces the expected `needs-revisit` set. Outer-loop: manual walkthrough judges whether cascade scope is appropriate (not too broad, not too narrow). | +| A49 | A modal secondary conversation thread can re-resolve all cascade implications from a knowledge-graph edit without requiring the user to restart the entire interview — the interviewer can meaningfully discuss design-tree implications in isolation from the original conversation flow | medium | D80, D84 | Knowledge-graph revisit, secondary threads | Validate with manual revisit walkthrough: invalidate a mid-graph item, confirm the secondary thread produces coherent re-resolution without losing context from the primary conversation. | ## Decisions + Pruned 2026-04-10: removed D33–D44 — client refactor decisions now embedded as structural + properties of the client architecture. Protected by I24. + Pruned 2026-04-10: removed D45–D48 — turn-response implementation details folded into D24. + Protected by I44. + Pruned 2026-04-10: removed D52–D56 — observer-widening-per-phase details folded into D13 and D25. + Protected by I54. + Pruned 2026-04-12: removed D51 — absorbed into D49 (same seam: kind-specific entity projection). + Removed D60 — absorbed into D57 (same seam: turn-response projection boundary). + Removed D62 — moot, Phase 5 done and ontology landed in 7a/7b. + Removed D63 — embedded, knowledge workspace shipped in 12a. + Removed D64 — moot, sequencing decision completed (11a, 12a, 12b all shipped). --> 30. **Vercel AI SDK replaces both Claude Agent SDK and raw Anthropic SDK** — `@ai-sdk/anthropic` provider with AI SDK primitives: `ToolLoopAgent` powers the interviewer (typed tools via `tool()` with Zod schemas, multi-step loop via `stopWhen`), `generateObject` powers the observer (structured extraction with Zod schema, no JSON parsing), `createUIMessageStream` + `pipeUIMessageStreamToResponse` handle server-side streaming, `validateUIMessages` validates incoming chat payloads. No hand-written stream translator, no DomainEvent layer on the web path. The `@anthropic-ai/sdk` package remains as a transitive dependency only. Depends on: —. Supersedes: Claude Agent SDK, raw Anthropic SDK approach, D27 (generator composition), D28 (outputFormat), D29 (ResultMessage metrics), custom agent loop plan (old D31). 32. **Core filesystem tools following pi-mono pattern** — 7 generic tools (read, write, edit, bash, grep, find, ls) in `src/server/tools/`, each a factory function returning an AI SDK `tool()` bound to a working directory. Tools are thin wrappers around Node.js fs APIs and shell commands (rg, fd), with truncation limits (500 lines / 64KB) following pi-mono's defaults. Composed via `createCoreTools(cwd)`. First use case: project characterization kickoff mode. Depends on: D30. Supersedes: —. -49. **Generic knowledge reads now surface canonical scope kinds and design commitments through the shared entity seam** — `knowledge_item` plus `turn_knowledge_item` are now the active persistence seam for all currently shipped knowledge writes, including canonical scope kinds plus `decision` / `assumption` commitments. The shared `/api/projects/:id/entities` projection and workspace surfaces still expose kind-specific `goals`, `terms`, `contexts`, `constraints`, `requirements`, `criteria`, `decisions`, and `assumptions` collections so the cutover can proceed without durable `framing` or a flat mixed list. Depends on: D5, D13, D22. Supersedes: decision/assumption-only entity reads in the sidebar and mixed storage assumptions at the read seam. +49. **Knowledge items persist generically but project through kind-specific collections at the entity API and workspace seams** — `knowledge_item` + `turn_knowledge_item` are the active persistence seam for all canonical knowledge writes. The shared `/api/projects/:id/entities` projection exposes kind-specific `goals`, `terms`, `contexts`, `constraints`, `requirements`, `criteria`, `decisions`, and `assumptions` collections rather than a flat mixed list, keeping tab-level affordances simple while preserving one generic storage model underneath. Depends on: D5, D13, D22. Supersedes: decision/assumption-only entity reads, mixed storage assumptions at the read seam, flat mixed generic projection. 50. **Generic dependency edges read through one typed entity-graph seam** — `knowledge_edge` now owns persisted dependency edges for the active knowledge graph, and `/api/projects/:id/entities` projects them as one typed `relationships[]` payload with explicit source/target identity (`collection`, `kind`, `id`) so workspace surfaces can render dependency affordances without consulting legacy decision/assumption edge tables. Depends on: D5, D13, D22, D49. Supersedes: flat entity reads with no graph relationship projection and legacy-table-owned dependency edges. -51. **Generic knowledge reads stay kind-specific at the workspace seam** — While generic knowledge items share one persistence table, the shared entities API and workspace surfaces should project `goal`, `term`, `context`, `constraint`, `requirement`, `criterion`, `decision`, and `assumption` as distinct collections rather than a flat mixed list. This keeps tab-level affordances simple while preserving one generic storage model underneath. Depends on: D22, D49, D50. Supersedes: flat mixed generic projection at the workspace seam. - -57. **Turn-response projection is one shared semantic boundary for downstream consumers** — Interviewer history and observer context should both read structured replies through one projection module that resolves selected option content from persisted `data-turn-response` parts. When that seam is absent, downstream consumers should treat `turn.answer` as display/compatibility copy rather than reconstructing structured reply semantics from `option.is_selected`. Depends on: D24, D25. Supersedes: ad hoc context-local response parsing, observer-only scalar answer projection, and selected-option fallback as a semantic compatibility seam. +57. **Turn-response projection is one shared semantic boundary for downstream consumers** — Interviewer history and observer context should both read structured replies through one projection module that resolves selected option content from persisted `data-turn-response` parts. Workspace response affordances (prompt visibility, "answered" state) derive from the presence of persisted `data-turn-response` user parts so selected-option replies and free-text-only replies share one seam after reload. When that seam is absent, downstream consumers should treat `turn.answer` as display/compatibility copy rather than reconstructing structured reply semantics from `option.is_selected`. Depends on: D24, D25. Supersedes: ad hoc context-local response parsing, observer-only scalar answer projection, selected-option fallback as a semantic compatibility seam, and `is_selected`-driven answered-state logic. 58. **Pending questions are a distinct workspace view model, not invented persisted turns** — Streamed `tool-ask_question` output now projects into a dedicated `pending-question` controller/view-state branch with its own ephemeral identity and option list, while persisted turn cards continue to use the durable `ProjectStateTurn` shape and submission flow. Route and UI layers render the union through one turn-card surface without inventing sentinel turn IDs, negative option IDs, or borrowed ancestry metadata. Depends on: D24, A16. Supersedes: fabricated persisted-turn projection for streamed interviewer questions. -59. **Knowledge-kind metadata lives behind one shared registry seam** — The active knowledge ontology should declare ordering, collection keys, labels, context headings, and empty-state copy in one shared registry module. Observer-result payload schemas, observer-created ID maps, entities projection, observer context sections, and user-facing knowledge surfaces should read from that registry instead of re-declaring parallel arrays or object shapes. Depends on: D13, D22, D25, D49, D51. Supersedes: duplicated knowledge-kind metadata across shared, server, and client seams. - -60. **Workspace response affordances derive from persisted turn responses, not selected-option flags** — Choice-turn cards still render durable option state, but prompt visibility and “answered” affordances should derive from the presence of persisted `data-turn-response` user parts so selected-option replies and free-text-only replies share one semantic seam after reload. Depends on: D24. Supersedes: `is_selected`-driven answered-state logic in workspace view-state. +59. **Knowledge-kind metadata lives behind one shared registry seam** — The active knowledge ontology should declare ordering, collection keys, labels, context headings, and empty-state copy in one shared registry module. Observer-result payload schemas, observer-created ID maps, entities projection, observer context sections, and user-facing knowledge surfaces should read from that registry instead of re-declaring parallel arrays or object shapes. Depends on: D13, D22, D25, D49. Supersedes: duplicated knowledge-kind metadata across shared, server, and client seams. 61. **The current mixed knowledge storage model is transitional, not architectural target state** — The current split between generic `knowledge_item` rows for some kinds and dedicated legacy tables for `decision` / `assumption` is a migration seam, not the intended end state. The target is one coherent generic knowledge model — `knowledge_item` + `turn_knowledge_item` + `knowledge_edge` + `knowledge_review`, all keyed by canonical `kind + subtype` metadata across the full ontology — and not a permanent hybrid or a return to one-table-per-kind storage. Legacy `decision` / `assumption` tables and their specialized edge tables are temporary compatibility seams to retire. Depends on: D5, D17, D50, D59. Supersedes: treating mixed legacy/generic storage as an acceptable steady state. -62. **Phase 5 may establish workflow closure before the knowledge ontology is finalized, but it must not harden the transitional ontology into the long-term review model** — Slice 7 (explicit phase outcomes + scope closure) can proceed on the current foundation because it establishes closure mechanics and readiness artifacts. Later mode/review slices must treat the current `framing`-based six-kind implementation as provisional until the sharper knowledge-layer design lands, and must avoid assuming that only decisions/assumptions participate in graph semantics. Depends on: D3, D5, D13, D17, D25, D59, D60. Supersedes: implicit assumption that the current Phase 4 ontology is stable enough to carry the full Phase 5 review model unchanged. - -63. **Phase 6 knowledge work is a broader workspace problem, not a sidebar-only edit path** — The sidebar may remain a compact summary or navigation surface, but it is not the final primary interface for reviewing, editing, and understanding the knowledge graph. The first non-sidebar workspace should be a phase-oriented review surface with list/detail inspection, provenance, relationship context, and readiness actions; it should not require a graph-canvas-first UI to become useful. Depends on: D22, D50, D59, D61. Supersedes: sidebar edits as the assumed long-term knowledge interaction model. - -64. **Dashboard workflow status and reviewed export can land before generalized revisit** — The first Phase 6 priority is surfacing durable workflow/readiness state and unlocking reviewed export from that state. Generalized revisit remains a required product capability, but it is explicitly deferred until the readiness/export path is stable enough to absorb invalidation and branch UX without widening the current phase. Depends on: D3, D17, D26, D63. Supersedes: requiring generalized revisit to land before the first readiness-surface and export improvements. - -65. **Explicit phase outcomes persist closure provenance, while workflow state projects status, closeability, and readiness from the active path** — A phase outcome should persist as a durable `phase_outcome` row with proposal-turn provenance, closure-turn linkage, summary text, closure basis, and a captured readiness-band snapshot at the moment of close rather than hiding workflow state behind `turn.is_resolution` or transient UI state. The current workflow state for each phase should project (a) lifecycle status (`unstarted` / `in_progress` / `closed` / `invalidated`), (b) deterministic phase-specific closeability, and (c) current readiness band from the active path instead of collapsing them into one opaque completion bit. Depends on: D3, D17, D62. Supersedes: D65-old workflow-state projection that exposed only proposal / confirmation state. +65. **Explicit phase outcomes persist closure provenance, while workflow state projects status, closeability, and readiness from the active path** — A phase outcome should persist as a durable `phase_outcome` row with proposal-turn provenance, closure-turn linkage, summary text, closure basis, and a captured readiness-band snapshot at the moment of close rather than hiding workflow state behind `turn.is_resolution` or transient UI state. The current workflow state for each phase should project (a) lifecycle status (`unstarted` / `in_progress` / `closed` / `invalidated`), (b) deterministic phase-specific closeability, and (c) current readiness band from the active path instead of collapsing them into one opaque completion bit. Depends on: D3, D17. Supersedes: D65-old workflow-state projection that exposed only proposal / confirmation state. -66. **Phase closing uses one transcript-friendly seam whether the interviewer recommends it or the user forces it** — The interviewer may either ask another structured question or emit a closure recommendation with a typed `data-phase-summary` artifact. Once the phase-specific closeability rule is satisfied, the user may either accept that recommendation or force-close the phase anyway; both paths should persist through the same chat-friendly seam with explicit closure basis so downstream UI and export can distinguish normal close from carried debt. Depends on: D24, D30, D62, D65. Supersedes: D66-old confirmation-only closure flow in which only the interviewer could initiate transition. +66. **Phase closing uses one transcript-friendly seam whether the interviewer recommends it or the user forces it** — The interviewer may either ask another structured question or emit a closure recommendation with a typed `data-phase-summary` artifact. Once the phase-specific closeability rule is satisfied, the user may either accept that recommendation or force-close the phase anyway; both paths should persist through the same chat-friendly seam with explicit closure basis so downstream UI and export can distinguish normal close from carried debt. Depends on: D24, D30, D65. Supersedes: D66-old confirmation-only closure flow in which only the interviewer could initiate transition. -67. **Scope closure stays anchored to a projected scope bundle, not a `framing` bucket** — Slice 7’s phase-outcome architecture remains valid if scope readiness reads from a projection over canonical `goal` / `term` / `context` / `constraint` items plus any unmigrated legacy `framing` rows during migration, rather than binding closure semantics to one transitional storage kind. Depends on: D3, D5, D61, D62, D65. Supersedes: coupling scope closure semantics to the durability of the current `framing` label. +67. **Scope closure stays anchored to a projected scope bundle, not a `framing` bucket** — Slice 7’s phase-outcome architecture remains valid if scope readiness reads from a projection over canonical `goal` / `term` / `context` / `constraint` items plus any unmigrated legacy `framing` rows during migration, rather than binding closure semantics to one transitional storage kind. Depends on: D3, D5, D61, D65. Supersedes: coupling scope closure semantics to the durability of the current `framing` label. 68. **`framing` is a migration-only intake alias, not a canonical durable kind** — New long-term writes should target canonical `goal`, `term`, `context`, and `constraint` items. Existing `framing` rows may remain readable through compatibility projections while they are normalized, reviewed, or superseded, but Phase 5/6 features should not require durable `framing` as a first-class destination. Depends on: D5, D61, D67. Supersedes: treating `framing` as part of the final ontology rather than a transitional intake seam. -69. **The first canonical knowledge workspace is phase-oriented list/detail review, not sidebar-only and not graph-canvas-first** — The main review surface should help the user normalize the scope bundle, inspect provenance and relationships, edit/review items, and understand readiness effects. Graph visualization can remain a later enhancement layered on top of the same canonical model rather than the first required workspace shape. Depends on: D17, D63, D67, D68. Supersedes: assuming the first non-sidebar knowledge surface must either stretch the sidebar or jump immediately to a freeform graph canvas. +69. **The first canonical knowledge workspace is phase-oriented list/detail review, not sidebar-only and not graph-canvas-first** — The main review surface should help the user normalize the scope bundle, inspect provenance and relationships, edit/review items, and understand readiness effects. Graph visualization can remain a later enhancement layered on top of the same canonical model rather than the first required workspace shape. Depends on: D17, D67, D68. Supersedes: assuming the first non-sidebar knowledge surface must either stretch the sidebar or jump immediately to a freeform graph canvas. 70. **First-pass closeability rules are deterministic, phase-specific, and intentionally minimal** — Closeability should start as an existential minimum bar the frontend can explain: a phase must have at least one phase-relevant captured signal before the user is allowed to close it. Readiness remains a separate descriptive band that may still be low when the user closes, which avoids both hidden model authority and false-precision percentage gates. Depends on: D65, D66. Supersedes: interviewer-only implicit closure thresholds. @@ -174,17 +169,27 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. 79. **The first requirements closeability seam is deterministic review coverage over explicit per-item requirement state, and it reuses the shared phase-close proposal transport** — Slice 9.4 should not introduce a requirements-only completion bit or readiness gate. Requirements become closeable once the current requirement set has no `pending` review state — i.e. every requirement is explicitly `approved` or `rejected` — while readiness remains a separate descriptive signal. Once closeable, requirements review reuses the shared `propose_phase_closure` / `data-phase-summary` / `phase_outcome` seam already established in earlier phases; criteria remains unopened until the proposal is explicitly confirmed. Depends on: D65, D66, D70, D77, D78. Supersedes: keeping requirements permanently non-closeable until a later bespoke workflow seam exists. +80. **Knowledge-graph revisit replaces hard turn-tree branching as the primary revisit mechanism** — Users revisit by editing knowledge items directly (invalidate/remove) in the knowledge workspace, not by forking the turn tree. Cascade invalidation traces downstream through knowledge graph edges (`depends_on`, `derived_from`, etc.), flagging affected items as `needs-revisit`. A modal secondary conversation thread is spawned to walk through design-tree implications, re-resolve affected items, and review downstream requirements/criteria. Affected phase outcomes reopen; the project is in an unfinished state until the secondary thread resolves all flagged items and phases are re-closed. One secondary thread at a time; the flow is strictly modal. Depends on: D5, D17, D65. Supersedes: turn-tree branching (requirements #9 old, #10 old) as the primary revisit model for V1. + +81. **Local-first storage in `.brunch/` project directory** — `npx brunch` creates a `.brunch/` directory in the current working directory (like `.git`), containing the SQLite database and configuration. Multiple elicitation runs / project versions coexist within one `.brunch/` directory. No global `~/.brunch/` store. Portability is a filesystem concern: copy the `.brunch/` directory. Depends on: D10. Supersedes: `~/.brunch/` global store option in D10. + +82. **First screen routes between greenfield and brownfield** — The initial project creation UI asks whether this is a new concept from scratch (greenfield) or a feature/sub-scope within an existing codebase (brownfield). Greenfield enters the scope interview directly. Brownfield triggers agent codebase exploration before the first interview turn. Depends on: D81, D32. Supersedes: implicit greenfield-only assumption. + +83. **Brownfield exploration feeds interviewer context, not the knowledge layer directly** — In brownfield mode, the interviewer agent explores the codebase using core tools (read, grep, find, ls, bash) and synthesizes discoveries into the first interview turn. The observer extracts knowledge items from that turn through the existing pipeline. No new pre-turn knowledge seeding mechanism. This keeps the architecture intact: the observer is the sole entry point for knowledge item creation. Depends on: D4, D30, D32, D82. Supersedes: pre-interview knowledge seeding. + +84. **Secondary conversation threads inherit validity from their primary-tree anchor** — A secondary thread is linked to the highest turn in the primary conversation that is implicitly associated (via knowledge item provenance) with the affected items. Knowledge items created or modified in the secondary thread are valid if and only if the anchor turn is on the active path of the primary conversation. This ensures that any future hard turn-tree branching above the anchor point would properly invalidate the secondary thread and its knowledge items. Depends on: D1, D80. Supersedes: —. + 26. **`md-pen` for programmatic markdown rendering** — Structured data (entity tables, dependency graphs, checklists) rendered to markdown via `md-pen` rather than hand-rolled string concatenation. Pure string-return functions (`table()`, `taskList()`, `mermaid()`, `heading()`, `alert()`, `details()`) compose by nesting — no AST, no intermediate representation. Escaping is context-aware per function (table cells, URLs, code fences), eliminating a class of bugs when rendering user-supplied text from interviews. Primary use cases: (1) observer context builders presenting growing entity graphs to agents (`table()` for decisions/assumptions with metadata, `taskList()` for reviewed/unreviewed items), (2) spec export rendering active-path entities into downloadable markdown (slice 13), (3) any future agent-facing or user-facing projection of structured data. Zero dependencies, ESM-only, TypeScript-first. Depends on: —. Supersedes: hand-rolled string assembly in context builders. ### Domain model -1. **Turn tree as version history** — The conversation is a tree, not a flat log. Each turn points to its parent. Revisiting an earlier turn forks a new branch. `project.active_turn_id` is the HEAD pointer. The active path determines which interview history and downstream readiness are current — no snapshot tables needed. Depends on: A6. Supersedes: D5-old snapshot versioning model. +1. **Turn list as primary conversation record** — The primary conversation is an ordered linked list of turns. Each turn points to its parent. `project.active_turn_id` is the HEAD pointer. The linked-list structure supports future branching structurally, but hard fork/checkout UX is deferred for V1. Revisit operates at the knowledge-graph level via edit mode + secondary threads (D80), not by forking the turn list. Depends on: A6. Supersedes: D5-old snapshot versioning model, turn-tree branching as primary revisit mechanism. 2. **Workflow phases are interview modes, not exclusive capture windows** — `scope`, `design`, `requirements`, and `criteria` are workflow modes that change interviewer behavior, observer extraction bias, and closure logic. Any mode may surface any knowledge kind, but each mode owns review and closure for the item families it is responsible for. Supersedes: phase model that implied entities first appear only inside their named phase. 3. **Explicit phase outcomes replace turn-local phase booleans as the target phase-closure model** — A phase is not truly closed because one turn was marked special; it is closed because the active path has a closed, non-invalidated phase outcome for that mode. Closeability and readiness are separate projections, not synonyms for closure. The current `turn.is_resolution` field is an implementation seam on the way to explicit `phase_outcome` records. Depends on: A15. Supersedes: pure latest-turn `is_resolution` semantics. -4. **Two-agent pattern (interviewer + observer)** — The interviewer focuses solely on conducting the interview. After each answered turn, a separate observer agent performs a structured extraction pass over the completed turn plus accumulated knowledge context. The observer can use a cheaper/faster model. Keeps the interviewer prompt clean and extraction independently testable. Depends on: A3, A4, A14. Supersedes: —. +4. **Two-agent pattern (interviewer + observer)** — The interviewer focuses solely on conducting the interview. After each answered turn, a separate observer agent performs a structured extraction pass over the completed turn plus accumulated knowledge context. The observer can use a cheaper/faster model. Keeps the interviewer prompt clean and extraction independently testable. Depends on: A3, A4. Supersedes: —. 5. **Canonical knowledge ontology expands beyond `framing` into eight reviewable kinds** — The target durable semantic layer is a typed knowledge graph with these canonical kinds: `goal`, `term`, `context`, `constraint`, `assumption`, `decision`, `requirement`, and `criterion`, with subtype support where needed. `framing` is a transitional intake label from the current implementation, not a final canonical durable kind. Items link back to source turns and connect through typed edges such as `depends_on`, `derived_from`, `constrains`, `verifies`, `motivates`, `defines`, and `refines`. Supersedes: separate decision / assumption graph as the sole semantic core, and the current six-kind `framing`-based ontology as the long-term target. 6. **Capture-anytime, review-in-phase** — Knowledge kinds may surface in any mode. Later modes synthesize and review rather than pretending to capture everything from scratch: scope closes sufficient shared understanding of goals, terms, context, and constraints; design closes commitment coherence; requirements closes requirement completeness; and criteria closes verification coverage. Supersedes: rigid first-capture boundaries between scope, design, requirements, and criteria. -17. **Soft invalidation tracks readiness staleness from turn-local frontiers** — Revisit invalidates trust from the affected turn frontier forward. Path exclusion (lazy): changing HEAD removes abandoned-branch artifacts from the active path. Readiness staleness (eager): downstream `phase_outcome` and per-item review records become stale or superseded when upstream knowledge they depend on changes. Cascades include: scope-knowledge change (`goal` / `term` / `context` / `constraint`) → design + later reviews stale; design change → requirement + criteria reviews stale; requirement change → criteria reviews stale. Supersedes: requirement/criterion-only invalidation model tied narrowly to `reviewed_at`. +17. **Soft invalidation traces through knowledge graph edges, not turn ancestry** — Revisit invalidates trust by walking dependency edges in the knowledge graph. When a knowledge item is invalidated or removed in edit mode, downstream items linked via `depends_on`, `derived_from`, etc. are flagged `needs-revisit`. Cascade direction follows the knowledge graph: scope-knowledge change (`goal` / `term` / `context` / `constraint`) → design + later reviews stale; design change → requirement + criteria reviews stale; requirement change → criteria reviews stale. Affected phase outcomes reopen. Resolution happens through a modal secondary conversation thread (D80). Path exclusion (lazy, via HEAD movement) remains for future hard turn-tree branching but is not the primary invalidation mechanism in V1. Supersedes: turn-local-frontier invalidation model, requirement/criterion-only invalidation tied narrowly to `reviewed_at`. 13. **Observer captures typed knowledge items plus derived intelligence, but the ontology is optimized for reviewability rather than capture convenience** — The observer's extraction mandate extends beyond decisions and assumptions to include goals, terms, current context, constraints, emerging requirements, criteria-like signals, and derived observations that the interviewer surfaced during the turn. Capture-time typing can be somewhat provisional, but the target ontology must support later review, graph navigation, and normalization rather than collapsing ambiguous scope material into one broad `framing` bucket. These items are persisted in the knowledge layer so subsequent context builders can inject them as context. Supersedes: decisions/assumptions-only observer ontology. 14. **Part-type rendering via AI Elements** — Client renders message parts using AI Elements copy-paste components: `Reasoning` (auto-open/close collapsible with duration), `MessageResponse` (streaming markdown via `streamdown`), `Tool` (7-state collapsible with status badges). `Conversation` provides auto-scroll. `PromptInput` provides `ChatStatus`-aware submit/stop button. shadcn/ui (radix-nova preset) + Tailwind 4 as the styling foundation. Depends on: A16, A17. Supersedes: hand-rolled inline-styled message rendering. 23. **Parts-based persistence model (UIMessage/ModelMessage split)** — Two separate data layers: (1) **UI render state** (`UIMessage.parts[]` JSON) persisted per turn for faithful resume — captures reasoning blocks, tool-call lifecycle states, text, and custom Data Parts. (2) **Inference context** (`ModelMessage`-equivalent) derived at call time by typed context builders, never persisted. The turn tree remains canonical for branching history; parts remain the source of truth for rendering. Prompt/response payload evolution can move independently of persisted UI parts. Research: `docs/research/chat-application-data-models-conversation-turns-structured-data-generative-ui-persistence.md`. Depends on: A22. Supersedes: D15's scalar-only persistence model. @@ -196,13 +201,13 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. 7. **SQLite via better-sqlite3** — Zero-config embedded DB. Turn tree, knowledge items, graph edges, and readiness artifacts all live in SQLite tables. Schema defined in Drizzle (see D18). Depends on: A5, A6. Supersedes: Dolt (docker-based). 8. **Express.js server emits AI SDK UI message streams directly** — The chat route validates incoming `BrunchUIMessage[]`, persists the new turn, merges the interviewer stream into `createUIMessageStream`, emits typed observer-result data parts in-band, and pipes the result to the response. No handwritten stream-translation layer remains on the web path. Depends on: A1, A19, D19. Supersedes: hand-rolled NDJSON and DomainEvent-to-SSE translation. 9. **React + Vite + @ai-sdk/react + @tanstack/react-router client** — `useChat` for conversation streaming. TanStack Router for type-safe routing with route loaders for data fetching on navigation (replaces manual `useEffect` hydration). Three routes for MVP: project list (`/`), interview workspace (`/project/:id`), export preview (`/project/:id/export`). See `docs/archive/BREADBOARD.md`. Depends on: A9, A10. Supersedes: Preact, both existing frontends, single-page no-routing layout. -10. **npx-launchable single-command distribution** — `bin` entry, launcher starts Express (serves built Vite assets + API on one port), opens browser. Single env var: `ANTHROPIC_API_KEY`. DB auto-created in project directory or `~/.brunch/`. Depends on: A8. Supersedes: multi-step Docker + env var setup. +10. **npx-launchable single-command distribution** — `bin` entry, launcher starts Express (serves built Vite assets + API on one port), opens browser. Single env var: `ANTHROPIC_API_KEY`. DB auto-created in local `.brunch/` directory (see D81). Depends on: A8. Supersedes: multi-step Docker + env var setup. 16. **Integer autoincrement primary keys** — All entity tables use `INTEGER PRIMARY KEY AUTOINCREMENT` instead of `TEXT` UUIDs. SQLite ROWID alias is simpler, matches the original DBML design, avoids UUID generation. No external systems reference these IDs. Client coerces to strings for `useChat` hydration (`turn-${id}-answer`, `turn-${id}-question`). Depends on: D7. Supersedes: `randomUUID()` TEXT PKs from slice 2. 18. **Drizzle ORM replaces raw DDL** — TypeScript schema definition (`drizzle/schema.ts`) is single source of truth for types, DDL, and migrations. Auto-applies from `drizzle/migrations/` at startup. Drizzle Studio available for DB inspection during development. Depends on: A18, D7. Supersedes: raw DDL strings in db.ts, DBML design document, hand-written TypeScript interfaces. 19. **Layered architecture with an AI SDK-native chat boundary** — Core interview orchestration is split into typed helpers (`prepareTurn`, `finalizeTurn`, context builders, persistence helpers) while Express owns the chat stream composition. The boundary between server and client is `BrunchUIMessage`, not a separate in-house event protocol. Observer-result data stays in-band on the same stream for cache coherence (see D22). CLI and MCP can still derive later from the stabilized domain operations, but the web path optimizes for the typed UI-message contract first. Depends on: A19, D8, D12. Supersedes: interview logic embedded in Express POST handler and the DomainEvent-to-SSE translation layer. 21. **oxlint + oxfmt + tsgolint replaces eslint + tsc** — oxlint for linting (including 59 type-aware rules via tsgolint, the Go-based TypeScript backend), oxfmt for formatting (single quotes, 110 width, sorted imports). `npm run fix` (lint:fix + fmt) is the fast inner loop; `npm run verify` (check + test + build) is the commit gate. `--type-check` flag replaces `tsc --noEmit`. Depends on: —. Supersedes: eslint (removed), separate `tsc --noEmit` step. 20. **CLI executable with subcommands** — `npx brunch` launches web UI (default). `npx brunch [command]` for CLI operations on the same DB. Future: sidecar MCP server. Depends on: D10, D19. Supersedes: web-only distribution model in D10. -22. **TanStack Query + in-band observer-result sync** — Observer-created entities sync to the React UI through typed `data-observer-result` parts on the existing chat stream. `useChat`'s `onData` callback invalidates the entity query for the active project; project-state refresh remains route-driven on stream completion. If the observer later becomes async, a dedicated `EventSource` remains the fallback. TanStack Query owns persisted entity state; the chat stream owns transient message state. TanStack DB remains unnecessary for the current server-authoritative model. Research: `docs/research/async-server-state-to-ui-sync-for-chat-observer-agents.md`. Depends on: A20, A21, D4, D9, D19. Supersedes: status-based sidebar refresh workarounds. +22. **TanStack Query + in-band observer-result sync** — Observer-created entities sync to the React UI through typed `data-observer-result` parts on the existing chat stream. `useChat`'s `onData` callback invalidates the entity query for the active project; project-state refresh remains route-driven on stream completion. If the observer later becomes async, a dedicated `EventSource` remains the fallback. TanStack Query owns persisted entity state; the chat stream owns transient message state. TanStack DB remains unnecessary for the current server-authoritative model. Research: `docs/research/async-server-state-to-ui-sync-for-chat-observer-agents.md`. Depends on: A20, D4, D9, D19. Supersedes: status-based sidebar refresh workarounds. ## Invariants @@ -274,7 +279,7 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. | # | Invariant | Established by | Protected by | Proves | | --- | ---------------------------------------------------------- | ------------------------- | ------------------------------------ | ----------- | -| I44 | Structured turn responses (zero/one/many selected options plus optional free-text) round-trip through persistence, transcript hydration, interviewer-history projection, observer-context projection, and workspace affordance state without collapsing back to scalar-only semantics | Slices 6d.1–6d.3; refactors 1, 3, 6; post-refactor cleanup | parts.test.ts, app.test.ts, context.test.ts, turn-response.test.ts, observer.test.ts, workspace-data.test.ts, InterviewWorkspace.test.tsx | D23, D24, D25, D39, D45, D46, D47, D48, D57, D60 | +| I44 | Structured turn responses (zero/one/many selected options plus optional free-text) round-trip through persistence, transcript hydration, interviewer-history projection, observer-context projection, and workspace affordance state without collapsing back to scalar-only semantics | Slices 6d.1–6d.3; refactors 1, 3, 6; post-refactor cleanup | parts.test.ts, app.test.ts, context.test.ts, turn-response.test.ts, observer.test.ts, workspace-data.test.ts, InterviewWorkspace.test.tsx | D23, D24, D25, D39, D45, D46, D47, D48, D57 | ### Generic knowledge seam @@ -283,7 +288,7 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. | # | Invariant | Established by | Protected by | Proves | | --- | ---------------------------------------------------------- | ------------------------- | ------------------------------------ | ----------- | -| I48 | Canonical knowledge kinds (`goal`, `term`, `context`, `constraint`, `requirement`, `criterion`, `decision`, `assumption`) persist with turn provenance, project through kind-specific entity collections and typed dependency edges, and surface through the shared knowledge registry without ontology drift or refresh regression — including scope-bundle coherence and compatibility collections | Slices 6e, 7b.1, 7b.2; refactor 5 (knowledge registry) | db.test.ts, app.test.ts, knowledge.test.ts, workspace-data.test.ts, workspace-controller.test.tsx, InterviewWorkspace.test.tsx | D5, D22, D49, D50, D51, D52, D53, D59, D61, D67, D68 | +| I48 | Canonical knowledge kinds (`goal`, `term`, `context`, `constraint`, `requirement`, `criterion`, `decision`, `assumption`) persist with turn provenance, project through kind-specific entity collections and typed dependency edges, and surface through the shared knowledge registry without ontology drift or refresh regression — including scope-bundle coherence and compatibility collections | Slices 6e, 7b.1, 7b.2; refactor 5 (knowledge registry) | db.test.ts, app.test.ts, knowledge.test.ts, workspace-data.test.ts, workspace-controller.test.tsx, InterviewWorkspace.test.tsx | D5, D22, D49, D50, D52, D53, D59, D61, D67, D68 | ### Observer widening seam @@ -312,7 +317,7 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. | # | Invariant | Established by | Protected by | Proves | | --- | ---------------------------------------------------------- | ------------------------- | ------------------------------------ | ----------- | -| I87 | Requirements-review mode grounds the interviewer in the current requirement inventory, targeted approve/reject actions persist durable active-path review links with latest-action-wins projection, closeability derives from full review coverage, and the shared phase-close proposal/confirmation seam reuses for requirements → criteria handoff with correct mode advancement | Slices 9.1–9.5 | context.test.ts, interview.test.ts, db.test.ts, app.test.ts, core.test.ts, EntitySidebar.test.tsx | D24, D25, D51, D61, D65, D66, D70, D71, D77, D78, D79, A28, A44, A45, A46 | +| I87 | Requirements-review mode grounds the interviewer in the current requirement inventory, targeted approve/reject actions persist durable active-path review links with latest-action-wins projection, closeability derives from full review coverage, and the shared phase-close proposal/confirmation seam reuses for requirements → criteria handoff with correct mode advancement | Slices 9.1–9.5 | context.test.ts, interview.test.ts, db.test.ts, app.test.ts, core.test.ts, EntitySidebar.test.tsx | D24, D25, D49, D61, D65, D66, D70, D71, D77, D78, D79, A28, A44 | ### Criteria-review seam @@ -343,9 +348,9 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. | Term | Definition | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **project** | A spec elicitation session. Has a name, a HEAD pointer (`active_turn_id`), and workflow/readiness state. | -| **turn** | A branching checkpoint in the interview history. Carries phase provenance plus typed interaction payloads and UI parts. Points to its parent turn — the turn tree is the version history. | -| **active path** | The branch from HEAD to root in the turn tree. Determines which turns, knowledge items, phase outcomes, and review state are currently trusted. | +| **project** | A spec elicitation run within a `.brunch/` directory. Has a name, a HEAD pointer (`active_turn_id`), and workflow/readiness state. Multiple projects can coexist in one `.brunch/` directory (different runs, versions, or feature scopes). | +| **turn** | A checkpoint in the interview history. Carries phase provenance plus typed interaction payloads and UI parts. Points to its parent turn. Turns belong to either the primary conversation or a secondary thread. | +| **active path** | The chain from HEAD to root in the primary conversation. Determines which turns, knowledge items, phase outcomes, and review state are currently trusted. Secondary threads inherit validity from their anchor turn on the active path. | | **phase** / **mode** | A workflow stage of the interview: `scope`, `design`, `requirements`, `criteria`. Modes change interviewer behavior, observer extraction bias, and closure logic. They are not exclusive capture windows. | | **choice turn** | An exploratory interaction turn where the interviewer proposes structured options and strategic grounding. Supports zero/one/many selections plus a unified free-text response field that is optional when options are chosen and required when none are chosen. | | **review turn** | A review interaction turn where the interviewer asks the user to approve, edit, reject, merge, or add to a synthesized item set. Early tracer bullets may approximate this with existing choice-turn responses before dedicated review-action payloads exist. | @@ -371,9 +376,14 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`. | **closure basis** | Provenance for how a phase was closed, such as interviewer-recommended close accepted by the user versus user-forced close once closeability was satisfied. | | **phase outcome** | The explicit workflow artifact for a mode: a proposed, closed, or superseded record tied back to the turn tree, carrying summary text, closure provenance, and any captured readiness snapshot. | | **knowledge review** | An explicit per-item review record (`pending`, `approved`, `edited`, `rejected`, `stale`) tied to the mode responsible for closing that item family. | -| **branch** (verb) | Fork the turn tree from a given turn, creating a new path and moving HEAD. Analogous to git branch + checkout. | -| **checkout** (verb) | Move HEAD to an existing turn on a different branch without creating new turns. Analogous to git checkout. | -| **soft invalidation** | Readiness staleness caused by upstream change. Some invalidation is lazy via path exclusion; some is eager via stale/superseded readiness records. Entities are not automatically deleted. See D17. | +| **branch** (verb) | Fork the turn tree from a given turn, creating a new path and moving HEAD. Analogous to git branch + checkout. Deferred for V1 — revisit operates at the knowledge-graph level instead. | +| **checkout** (verb) | Move HEAD to an existing turn on a different branch without creating new turns. Analogous to git checkout. Deferred for V1. | +| **edit mode** | A modal state in the knowledge workspace where the user can invalidate or remove knowledge items. Exiting edit mode triggers cascade calculation and spawns a secondary thread if items are affected. See D80. | +| **secondary thread** | A modal conversation thread spawned to re-resolve knowledge graph implications after edit-mode changes. Anchored to the highest turn in the primary conversation associated with affected items. Carries its own turns; knowledge items created here inherit validity from the anchor. See D80, D84. | +| **needs-revisit** | Flag on a knowledge item indicating it has been affected by an upstream invalidation/removal and must be resolved (modified, replaced, or confirmed still valid) through a secondary thread before the project can return to a complete state. | +| **greenfield** | A project starting from a blank concept with no existing codebase context. The default first-screen path. | +| **brownfield** | A project within an existing codebase. The first-screen brownfield path triggers agent codebase exploration before the first interview turn. See D82, D83. | +| **soft invalidation** | Readiness staleness caused by upstream change in the knowledge graph. Cascade traces through knowledge graph edges (D17). Affected phase outcomes reopen. Entities are not automatically deleted — they are flagged `needs-revisit` for explicit re-resolution. | | **interviewer** | The primary agent role: conducts the interview and review modes. It should propose structure, ask for rationale, and surface tradeoffs; it does not own semantic extraction. | | **observer** | The secondary agent role: performs a structured extraction pass after each answered turn, producing knowledge items and graph edges from a phase-aware context projection. | | **core** | The interface-agnostic service layer between the database and transport adapters. Owns turn preparation/finalization, context building, project state, knowledge lifecycle, readiness lifecycle, and export. | @@ -450,7 +460,7 @@ End-to-end slices must be **user-testable**, not just programmatically tested. E | Fast unit tests — DB | Turn persistence with phase provenance, entity writes with dependency edges | I5, I6, I9, I10, I11 | ms | | Fast unit tests — core | DomainEvent streaming, core/adapter separation, structured turn creation | I12, I13 | ms | | Fast unit tests — parts | Parts round-trip (DomainEvents → assemble → persist JSON → load → hydrate); Data Part schema validation (Zod parse on structured user input); context builder output shape | I17, I18, I19 | ms | -| Fast unit tests — turn response | Structured turn-response schema and submit seams establish zero/one/many selected-option arrays plus the required-free-text rule; interviewer context projection stays response-shaped, not scalar-only | I17, I18, I19, I44, A33 | ms | +| Fast unit tests — turn response | Structured turn-response schema and submit seams establish zero/one/many selected-option arrays plus the required-free-text rule; interviewer context projection stays response-shaped, not scalar-only | I17, I18, I19, I44 | ms | | Fast unit tests — observer sync | `observer-complete` emitted post-commit with entity IDs matching DB state; SSE adapter encodes as typed data part | D22, A20 | ms | | Fast unit tests — phase outcome lifecycle | Explicit phase-outcome records persist proposal/confirmation state, derive current readiness from the active path, and supersede correctly when upstream turns change | I18, I24, I72 | ms | | Type-aware linting | Semantic static checks (oxlint + tsgolint) | All | ms | @@ -459,15 +469,15 @@ End-to-end slices must be **user-testable**, not just programmatically tested. E | Oracle family | What it proves | Protects | Cost | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------------------------- | -| Differential testing (observer) | Observer extraction meets ≥80% entity capture rate against golden master fixtures | A14 | seconds per fixture; requires Claude API | -| Round-trip oracle (turn response) | Structured turn response survives submit → persistence → hydration → interviewer-context composition with no drift | I17, I18, I19, I22, I44, A33 | seconds | +| Differential testing (observer) | Observer extraction meets ≥80% entity capture rate against golden master fixtures | I48, I54 | seconds per fixture; requires Claude API | +| Round-trip oracle (turn response) | Structured turn response survives submit → persistence → hydration → interviewer-context composition with no drift | I17, I18, I19, I22, I44 | seconds | | Round-trip oracle (turn tree) | Structured turns → active path → entity resolution intact | I6, I9, I10 | ms | | Integration tests | SSE stream contains expected event types in order; DB lifecycle survives close/reopen | I2, I5, I13, I14 | seconds | | Round-trip oracle (observer sync) | Mocked observer widening survives result schema → persistence → entities API → sidebar refresh with no drift | I20, I21, I23, A20 | seconds | | Round-trip oracle (phase outcome) | Mocked phase-closure proposals plus user confirmation survive submit → persistence → project reload → workspace workflow-state projection with no drift, including the first requirements closeability/proposal path | I18, I24, I72, I87 | seconds | | Model-based lifecycle oracle (phase outcome) | A tiny reference state machine (`none → proposed → confirmed → superseded/stale`) matches real readiness behavior across refresh, revisit, and active-path changes, including requirements staying `in_progress` while proposal is pending | I24, I72, I87, A15 | seconds | | Contract testing (requirements-review grounding) | The first requirements-review tracer bullet proves two contracts independently: the current requirement inventory reaches the requirements-mode interviewer context, and the emitted review turn stays requirements-shaped rather than falling back to generic design follow-up behavior | I19, A44 | seconds | -| Round-trip oracle (requirements review) | A requirements-review turn plus a user reply about either a missing requirement, a targeted approval, or a targeted rejection survives submit → persistence → requirement review linkage / observer result → entities API → workspace/sidebar refresh with no drift | I18, I21, I23, I87, A44, A45 | seconds | +| Round-trip oracle (requirements review) | A requirements-review turn plus a user reply about either a missing requirement, a targeted approval, or a targeted rejection survives submit → persistence → requirement review linkage / observer result → entities API → workspace/sidebar refresh with no drift | I18, I21, I23, I87, A44 | seconds | | Model-based lifecycle oracle (requirements review) | A tiny reference rule proves the first requirements-review interactions leave `requirements` active, `in_progress`, and not yet closeable rather than accidentally reusing scope/design closure semantics | I24, I87, A15, A44 | seconds | **Outer loop** (minutes–hours): human observer @@ -480,7 +490,7 @@ End-to-end slices must be **user-testable**, not just programmatically tested. E | Fixture capture from manual runs | Bootstrap golden master fixtures by querying DB after confirmed-good sessions | Human judgment + SQL query | | Rich chat rendering | Tool call states, reasoning collapse, message parts render by type | Human + `/cli-cdp` | | Resume test | Close/reopen browser, verify state intact | Human + browser | -| Observer → sidebar reactivity | `onData` → query invalidation updates sidebar after observer extraction; validates A21 | Human + `/cli-cdp` (slice 6) | +| Observer → sidebar reactivity | `onData` → query invalidation updates sidebar after observer extraction | Human + `/cli-cdp` (slice 6) | | Manual phase-closure walkthrough | The interviewer proposes scope closure at sensible moments, the summary is understandable, and confirmation/transition UX is coherent | Human + browser (after 7a) | | Manual requirements-review walkthrough | The requirements-review turn feels grounded in the current requirement set, the reused choice-turn UI is acceptable for completeness review, the first explicit approve/reject actions remain legible as review state rather than deletion or invalidation, and the first close proposal feels timely once review coverage is complete | Human + browser | @@ -526,10 +536,10 @@ This projection difference is a deliberate design choice, not an implementation | Observer extraction variance | Spike measures capture rate single-shot per fixture; multi-run variance not measured. | Acceptable for initial delivery. | If extraction consistency degrades as history grows. | | Cumulative entity graph integrity | Individual extractions may be correct but compose into an incoherent graph over 15-20 turns. No programmatic check for drift. | Debug mode (human eyeballs the growing graph). Future: structural property tests (no orphaned edges, no DAG cycles, monotonic entity count). | After observer slice lands and manual testing reveals graph-level issues. | | Ontology sharpness / kind discrimination | The generic knowledge-item typology is still being pressure-tested for semantic separation and naming quality, so a schema-valid observer result may still be conceptually wrong. | Treat 6f.1 as structural-only in the middle loop; use manual walkthroughs to judge whether extracted `framing` is truly framing and capture confirmed-good sessions as future fixtures. | Revisit once several good/bad observer examples have been captured and the kind vocabulary feels stable. | -| Phase transition UX | Summary quality, resolution timing, confirmation flow. Fully visual. Slice 7 intentionally defers this outer-loop judgment until after 7a so the ontology/workspace redesign can sharpen what “ready to close scope” should mean. | Middle loop proves only proposal contract, persistence, active-path projection, and supersession semantics for slice 7; manual phase-closure walkthrough lands after 7a. | Revisit immediately after 7a, or sooner if the structural build reveals the proposal/confirmation shape is itself unstable. | +| Phase transition UX | Summary quality, resolution timing, confirmation flow. Fully visual. | Middle loop proves proposal contract, persistence, projection, and supersession. Manual phase-closure walkthroughs now possible with rich fixture scenarios (11c). | Revisit during outer-loop testing before distribution (slice 14). | | Requirements-review prompt grounding | Slice 9.1 proves the requirement inventory reaches the interviewer seam and the review loop stays coherent, but it does not force the emitted review question to quote or deeply reason over specific requirement text. | Manual requirements-review walkthrough judges whether the first review turn feels grounded enough; promote to a stronger contract or fixture-backed oracle if it feels generic. | Revisit if manual runs show requirements-review prompts drifting into generic follow-up instead of requirement-set review. | | Per-item requirement review semantics | Slices 9.2 and 9.3 now prove explicit requirement-level approval and rejection actions with durable `approved` / `rejected` / `pending` projection, but they still do not cover edit/add-missing payloads, merge semantics, or stale-state invalidation. | Keep the first per-item slices narrow; add focused review-action seams and oracles for edit/add/merge/stale behavior in later tracer bullets. | Revisit when scoping the next tracer bullet inside slice 9. | -| Criteria synthesis after requirements handoff | Slice 9.5 now proves the structural confirmation/handoff seam into `criteria`, but it does not yet prove that the first criteria-mode question is well grounded in the approved requirement set or that criteria closeability should depend on richer coverage than simple mode entry. | Use slice 10 to add criteria-specific grounding and lifecycle oracles; keep manual walkthroughs focused on whether the first criteria turn feels connected to the reviewed requirement set. | Revisit when building slice 10 criteria-review mode. | +| Criteria synthesis after requirements handoff | Slices 10.1–10.3 now prove criteria grounding in approved requirements, per-criterion review state, and criteria closure through the shared seam. Not yet proven: whether criteria synthesis quality holds across varied project types. | Middle loop covers structural grounding + lifecycle. Manual walkthroughs now possible with rich fixture scenarios (11c). | Revisit during outer-loop testing before distribution (slice 14). | | Performance under realistic load | 20+ turns, growing history summaries, observer latency. No budget oracle. | Acceptable for single-user tool. | If latency becomes noticeable during manual testing. | | `onData` stale-closure correctness | The workspace seam now has a component-level integration oracle, but it still mocks `useChat` and does not prove the exact live browser/runtime behavior of the AI SDK hook. Known `onFinish` stale-closure bug (ai-sdk#550) may still affect production wiring. | `InterviewWorkspace.test.tsx` protects the app-side invalidation logic; manual outer-loop validation remains required for live browser/runtime confirmation. If broken, fall back to parallel `EventSource` (D22 Option 2). | If sidebar fails to update after observer extraction during manual testing. | | Parts/scalar consistency | Persisted `assistant_parts` and scalar fields (`question`, `why`, `impact`, options) are two representations of the same turn content. No programmatic check that they agree. | Acceptable for initial delivery — scalars are written by MCP tool handler, parts assembled from stream. Both derive from the same `query()` call. Future: metamorphic oracle (text in parts matches scalars). | If turns appear correct in one view (parts-based UI) but wrong in another (scalar-based entity queries or export). | @@ -562,20 +572,24 @@ This projection difference is a deliberate design choice, not an implementation | message.test.tsx | 2 | I24, I27 | | build-boundary.test.ts | 1 | I24, I28, I30, I32 | | capability-boundaries.test.ts | 2 | I24, I29 | -| KnowledgeWorkspace.test.tsx | 4 | I48 | -| export.test.ts | 4 | D26, D65, D66, D70 | +| KnowledgeWorkspace.test.tsx | 5 | I24, I48 | +| workspace-loader.test.ts | 2 | I24 | +| export-loader.test.ts | 1 | D26, D65, D66, D70 | +| ExportPreview.test.tsx | 2 | D26, D65, D66, D70 | +| export.test.ts | 6 | D26, D65, D66, D70 | ## Acceptance Criteria (exit conditions) -1. `npx brunch` with `ANTHROPIC_API_KEY` in scope opens a working app in the browser -2. Starting a new project launches an interview in scope mode with structured exploratory turns that support rationale and custom answers -3. The observer extracts typed knowledge items and graph edges from each answered turn, visible through a durable knowledge surface rather than only transient chat state -4. The knowledge graph is navigable — user can inspect what important items depend on, derive from, constrain, motivate, define, or verify, using a fit-for-purpose knowledge workspace rather than a narrow sidebar alone -5. Each workflow mode surfaces closeability and readiness clearly, allows close once the minimum bar is met, and records an explicit phase outcome with summary and closure basis -6. Revisiting an upstream turn forks the turn tree and soft-invalidates downstream readiness from the affected frontier -7. Abandoning a branch restores the previous active path +1. `npx brunch` in a project directory with `ANTHROPIC_API_KEY` in scope opens a working app in the browser, with state in local `.brunch/` +2. First screen offers greenfield vs brownfield choice; brownfield triggers agent codebase exploration that grounds the first interview turn +3. Starting a new project launches an interview in scope mode with structured exploratory turns that support rationale and custom answers +4. The observer extracts typed knowledge items and graph edges from each answered turn, visible through a durable knowledge surface rather than only transient chat state +5. The knowledge graph is navigable — user can inspect what important items depend on, derive from, constrain, motivate, define, or verify, using a fit-for-purpose knowledge workspace rather than a narrow sidebar alone +6. Each workflow mode surfaces closeability and readiness clearly, allows close once the minimum bar is met, and records an explicit phase outcome with summary and closure basis +7. Invalidating or removing a knowledge item in edit mode traces cascade through knowledge graph edges, flags affected items, reopens affected phases, and spawns a secondary conversation thread that re-resolves implications 8. Requirements review mode synthesizes the requirement set from the knowledge layer, surfaces gaps, and records explicit review state 9. Criteria review mode synthesizes verification conditions from approved requirements plus the knowledge layer, and records explicit review state 10. Export produces valid markdown spec only when all workflow modes are resolved, all in-scope requirement/criteria reviews are complete, and no unresolved upstream staleness remains 11. Closing and reopening the browser resumes the interview from the active turn with knowledge and readiness state intact 12. All inner and middle loop tests pass +13. Partial-scope elicitation works — specifying a feature area within an existing project produces a coherent scoped spec diff --git a/package-lock.json b/package-lock.json index a2e433d6..e90a1347 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "@ai-sdk/anthropic": "^3.0.66", "@ai-sdk/react": "^3.0.145", "@anthropic-ai/sdk": "^0.82.0", - "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/inter": "^5.2.8", + "@ladle/react": "^5.1.1", "@modelcontextprotocol/sdk": "^1.27.1", "@radix-ui/react-use-controllable-state": "^1.2.2", "@streamdown/cjk": "^1.0.3", @@ -35,6 +36,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-resizable-panels": "^4.10.0", "shiki": "^4.0.2", "streamdown": "^2.5.0", "tailwind-merge": "^3.5.0", @@ -1795,10 +1797,10 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@fontsource-variable/geist": { + "node_modules/@fontsource-variable/inter": { "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.8.tgz", - "integrity": "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -2401,7 +2403,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2411,7 +2412,6 @@ "version": "5.1.21", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2433,7 +2433,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2461,7 +2460,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2476,7 +2474,6 @@ "version": "1.0.15", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2486,63 +2483,817 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ladle/react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@ladle/react/-/react-5.1.1.tgz", + "integrity": "sha512-HA3djOTK/CRWTdXzQ7sCu/6tmeYGZpRKTNH5hTvVqXH/Qxsnrguscz5uALWiGxcG8b/GAoU1HKbYTo5f53tTBw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/core": "^7.26.0", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.4", + "@babel/types": "^7.26.3", + "@ladle/react-context": "^1.0.1", + "@mdx-js/mdx": "^3.1.0", + "@mdx-js/react": "^3.1.0", + "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react-swc": "^3.7.2", + "axe-core": "^4.10.2", + "boxen": "^8.0.1", + "chokidar": "^4.0.3", + "classnames": "^2.5.1", + "commander": "^12.1.0", + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "get-port": "^7.1.0", + "globby": "^14.0.2", + "history": "^5.3.0", + "koa": "^2.15.4", + "lodash.merge": "^4.6.2", + "msw": "^2.7.0", + "open": "^10.1.0", + "prism-react-renderer": "^2.4.1", + "prop-types": "^15.8.1", + "query-string": "^9.1.1", + "react-hotkeys-hook": "^4.6.1", + "react-inspector": "^6.0.2", + "rehype-class-names": "^2.0.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.0", + "source-map": "^0.7.4", + "vfile": "^6.0.3", + "vite": "^6.0.5", + "vite-tsconfig-paths": "^5.1.4" + }, + "bin": { + "ladle": "lib/cli/cli.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@ladle/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@ladle/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-xVQ8siyOEQG6e4Knibes1uA3PTyXnqiMmfSmd5pIbkzeDty8NCBtYHhTXSlfmcDNEsw/G8OzNWo4VbyQAVDl2A==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, + "node_modules/@ladle/react/node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@ladle/react/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ladle/react/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/@ladle/react/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ladle/react/node_modules/react-inspector": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz", + "integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@ladle/react/node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ladle/react/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@ladle/react/node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">=18" + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@ladle/react/node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", + "node_modules/@mdx-js/mdx/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">=6.0.0" + "node": ">= 12" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" } }, "node_modules/@mermaid-js/parser": { @@ -2620,7 +3371,6 @@ "version": "0.41.3", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", - "dev": true, "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", @@ -2844,7 +3594,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2858,7 +3607,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2868,7 +3616,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2882,14 +3629,12 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", @@ -2900,7 +3645,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, "license": "MIT" }, "node_modules/@opentelemetry/api": { @@ -5758,32 +6502,248 @@ "@types/hast": "^3.0.4" } }, - "node_modules/@streamdown/math": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@streamdown/math/-/math-1.0.2.tgz", - "integrity": "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==", - "license": "Apache-2.0", - "dependencies": { - "katex": "^0.16.27", - "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" + "node_modules/@streamdown/math": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@streamdown/math/-/math-1.0.2.tgz", + "integrity": "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==", + "license": "Apache-2.0", + "dependencies": { + "katex": "^0.16.27", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@streamdown/mermaid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@streamdown/mermaid/-/mermaid-1.0.2.tgz", + "integrity": "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==", + "license": "Apache-2.0", + "dependencies": { + "mermaid": "^11.12.2" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz", + "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz", + "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz", + "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz", + "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz", + "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz", + "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz", + "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz", + "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz", + "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz", + "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz", + "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@streamdown/mermaid": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@streamdown/mermaid/-/mermaid-1.0.2.tgz", - "integrity": "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==", - "license": "Apache-2.0", - "dependencies": { - "mermaid": "^11.12.2" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz", + "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -5795,6 +6755,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", @@ -6681,6 +7650,12 @@ "@types/unist": "*" } }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -6704,6 +7679,12 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", @@ -6722,7 +7703,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6763,7 +7743,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", - "dev": true, "license": "MIT" }, "node_modules/@types/superagent": { @@ -6872,6 +7851,65 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@vitejs/plugin-react-swc/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react-swc/node_modules/@swc/core": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz", + "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.24", + "@swc/core-darwin-x64": "1.15.24", + "@swc/core-linux-arm-gnueabihf": "1.15.24", + "@swc/core-linux-arm64-gnu": "1.15.24", + "@swc/core-linux-arm64-musl": "1.15.24", + "@swc/core-linux-ppc64-gnu": "1.15.24", + "@swc/core-linux-s390x-gnu": "1.15.24", + "@swc/core-linux-x64-gnu": "1.15.24", + "@swc/core-linux-x64-musl": "1.15.24", + "@swc/core-win32-arm64-msvc": "1.15.24", + "@swc/core-win32-ia32-msvc": "1.15.24", + "@swc/core-win32-x64-msvc": "1.15.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", @@ -6917,16 +7955,6 @@ } } }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vitest/pretty-format": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", @@ -7020,6 +8048,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -7128,11 +8165,19 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7142,7 +8187,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -7214,6 +8258,15 @@ "node": ">=4" } }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7221,6 +8274,15 @@ "dev": true, "license": "MIT" }, + "node_modules/axe-core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", + "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -7273,6 +8335,16 @@ "node": ">=6.0.0" } }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/better-sqlite3": { "version": "12.8.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", @@ -7331,6 +8403,137 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -7348,7 +8551,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7425,7 +8627,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -7446,6 +8647,40 @@ "node": ">= 0.8" } }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/cache-content-type/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cache-content-type/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -7485,6 +8720,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001782", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", @@ -7614,6 +8861,21 @@ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -7632,6 +8894,24 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -7665,7 +8945,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" @@ -7683,7 +8962,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -7719,6 +8997,16 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -7726,11 +9014,20 @@ "dev": true, "license": "MIT" }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -7743,7 +9040,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -7895,6 +9191,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -7962,6 +9271,22 @@ "node": ">= 8" } }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7979,7 +9304,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -8548,6 +9872,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -8578,6 +9911,12 @@ } } }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -8601,7 +9940,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -8618,7 +9956,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8631,7 +9968,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8659,6 +9995,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8677,6 +10019,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -8726,6 +10078,19 @@ "node": ">=0.3.1" } }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -9429,7 +10794,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -9548,6 +10912,38 @@ "node": ">= 0.4" } }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esbuild": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", @@ -9630,6 +11026,35 @@ "node": ">=4" } }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", @@ -9640,6 +11065,67 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -9793,7 +11279,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -9833,7 +11318,6 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -9906,7 +11390,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -9915,6 +11398,18 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -10103,6 +11598,15 @@ "dev": true, "license": "MIT" }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10116,7 +11620,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -10180,6 +11683,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -10233,7 +11748,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -10242,6 +11756,53 @@ "node": ">= 6" } }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10264,7 +11825,6 @@ "version": "16.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", - "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -10333,7 +11893,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -10357,6 +11916,20 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-classnames": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-classnames/-/hast-util-classnames-3.0.0.tgz", + "integrity": "sha512-tI3JjoGDEBVorMAWK4jNRsfLMYmih1BUOG3VV36pH36njs1IEl7xkNrVTD2mD2yYHmQCa5R/fj61a8IAF4bRaQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-from-dom": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", @@ -10426,6 +11999,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", @@ -10492,6 +12078,61 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -10561,6 +12202,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-text": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", @@ -10611,9 +12265,17 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true, "license": "MIT" }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hono": { "version": "4.12.9", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", @@ -10643,6 +12305,53 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -10840,7 +12549,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -10856,7 +12564,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10866,17 +12573,34 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -10912,7 +12636,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -10944,14 +12667,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10988,6 +12709,24 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-regexp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", @@ -11031,7 +12770,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -11148,60 +12886,254 @@ "json5": "lib/cli.js" }, "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.44", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.44.tgz", + "integrity": "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/koa": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz", + "integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/koa/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, + "node_modules/koa/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">= 0.6" } }, - "node_modules/katex": { - "version": "0.16.44", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.44.tgz", - "integrity": "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], + "node_modules/koa/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "mime-db": "1.52.0" }, - "bin": { - "katex": "cli.js" + "engines": { + "node": ">= 0.6" } }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/koa/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 0.6" } }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, + "node_modules/koa/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, "engines": { - "node": ">=6" + "node": ">= 0.6" } }, "node_modules/langium": { @@ -11501,6 +13433,12 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -11554,6 +13492,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -11592,6 +13542,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -11792,6 +13754,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -11953,7 +13932,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -12278,6 +14256,108 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -12321,6 +14401,33 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, "node_modules/micromark-factory-space": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", @@ -12522,6 +14629,31 @@ ], "license": "MIT" }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", @@ -12655,7 +14787,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -12669,7 +14800,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12845,7 +14975,6 @@ "version": "2.12.14", "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.14.tgz", "integrity": "sha512-4KXa4nVBIBjbDbd7vfQNuQ25eFxug0aropCQFoI0JdOBuJWamkT1yLVIWReFI8SiTRc+H1hKzaNk+cLk2N9rtQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -12890,7 +15019,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12904,14 +15032,12 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, "license": "MIT" }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" @@ -13168,6 +15294,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13264,6 +15402,11 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" + }, "node_modules/open": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", @@ -13380,7 +15523,6 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, "license": "MIT" }, "node_modules/oxfmt": { @@ -13615,6 +15757,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -13821,6 +15975,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13845,6 +16012,23 @@ "node": ">=6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -13884,20 +16068,36 @@ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz", + "integrity": "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" }, "engines": { - "node": ">=0.6" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -14060,6 +16260,16 @@ "react": "^19.2.4" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.2.tgz", + "integrity": "sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14124,6 +16334,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.10.0.tgz", + "integrity": "sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -14160,6 +16380,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -14177,6 +16410,73 @@ "node": ">= 4" } }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -14201,6 +16501,18 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, + "node_modules/rehype-class-names": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-class-names/-/rehype-class-names-2.0.0.tgz", + "integrity": "sha512-jldCIiAEvXKdq8hqr5f5PzNdIDkvHC6zfKhwta9oRoMu7bn0W7qLES/JrrjBvr9rKz3nJ8x4vY1EWI+dhjHVZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-classnames": "^3.0.0", + "hast-util-select": "^6.0.0", + "unified": "^11.0.4" + } + }, "node_modules/rehype-harden": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/rehype-harden/-/rehype-harden-1.1.8.tgz", @@ -14244,6 +16556,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-sanitize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", @@ -14334,6 +16661,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -14392,7 +16733,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14448,14 +16788,12 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", - "dev": true, "license": "MIT" }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -14544,7 +16882,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14557,7 +16894,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -14613,6 +16949,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -14960,7 +17313,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -15021,6 +17373,18 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15061,6 +17425,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -15141,7 +17517,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, "license": "MIT" }, "node_modules/string_decoder": { @@ -15157,7 +17532,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15204,7 +17578,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15351,7 +17724,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -15492,7 +17864,6 @@ "version": "7.0.27", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", - "dev": true, "license": "MIT", "dependencies": { "tldts-core": "^7.0.27" @@ -15505,14 +17876,12 @@ "version": "7.0.27", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", - "dev": true, "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -15534,7 +17903,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" @@ -15599,6 +17967,26 @@ "code-block-writer": "^13.0.3" } }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -15620,6 +18008,15 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -15665,7 +18062,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", - "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -15695,7 +18091,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -15722,7 +18118,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -15790,6 +18185,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-remove-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", @@ -15869,7 +18277,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/kettanaito" @@ -16133,6 +18540,25 @@ "vite": ">=5.0.0" } }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", @@ -16326,11 +18752,75 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -16393,7 +18883,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -16409,7 +18898,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -16428,12 +18916,20 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", @@ -16451,7 +18947,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 8e0fc388..e2210a12 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,15 @@ "studio": "drizzle-kit studio", "test": "vitest run", "typecheck": "tsc --noEmit --project tsconfig.tools.json", + "ladle": "ladle serve --port 61000 --viteConfig vite.config.ts", "verify": "npm run check && npm run test && npm run build" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.66", "@ai-sdk/react": "^3.0.145", "@anthropic-ai/sdk": "^0.82.0", - "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/inter": "^5.2.8", + "@ladle/react": "^5.1.1", "@modelcontextprotocol/sdk": "^1.27.1", "@radix-ui/react-use-controllable-state": "^1.2.2", "@streamdown/cjk": "^1.0.3", @@ -57,6 +59,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-resizable-panels": "^4.10.0", "shiki": "^4.0.2", "streamdown": "^2.5.0", "tailwind-merge": "^3.5.0", diff --git a/src/client/components/app-shell.stories.tsx b/src/client/components/app-shell.stories.tsx new file mode 100644 index 00000000..6d1b15e4 --- /dev/null +++ b/src/client/components/app-shell.stories.tsx @@ -0,0 +1,265 @@ +import type { Story, StoryDefault } from '@ladle/react'; +import { useState } from 'react'; + +import { + EmptyCard, + PhaseSidebar, + ShellButton, + StageSidebar, + TabSwitcher, + type Phase, + type PhaseStatus, + type StageItem, +} from './app-shell'; + +export default { + title: 'Shell', +} satisfies StoryDefault; + +// ── Stage sidebar ──────────────────────────────────────────────────── + +const sampleStages: StageItem[] = [ + { + key: 'scope', + label: 'Project Spec', + status: 'done', + children: [ + { key: 'scope-defining', label: 'Defining', status: 'done' }, + { key: 'scope-clarify', label: 'Clarify specification', status: 'done' }, + ], + }, + { key: 'assumptions', label: 'Assumptions', status: 'active' }, + { key: 'spec-overview', label: 'Specification Overview', status: 'future' }, + { key: 'requirements', label: 'Requirements', status: 'future' }, + { key: 'gaps', label: 'Gaps & Surprises', status: 'future' }, +]; + +const samplePhases: Record = { + scope: 'closed', + design: 'in_progress', + requirements: 'unstarted', + criteria: 'unstarted', +}; + +export const StageSidebarExpanded: Story = () => { + return ( +
+ +
+

Main content area

+
+
+ ); +}; + +export const PhaseSidebarCollapsed: Story = () => { + return ( +
+ +
+

Main content area

+
+
+ ); +}; + +export const SidebarToggle: Story = () => { + const [expanded, setExpanded] = useState(true); + + return ( +
+ {expanded ? ( + setExpanded(false)} + /> + ) : ( + setExpanded(true)} /> + )} +
+

Click the collapse/expand button to toggle.

+
+
+ ); +}; + +// ── Empty states ───────────────────────────────────────────────────── + +export const EmptyStates: Story = () => { + return ( +
+

Empty state patterns

+ + {/* Pattern 1: Text only */} +
+

1. Simple — text only

+
+ + +
+
+ + {/* Pattern 2: With CTA */} +
+

2. With call to action

+
+ +
+ Start interview +
+
+
+
+ + {/* Pattern 3: Centered hero */} +
+

3. Centered hero

+
+

No conversation yet

+

+ Begin the interview to start building your specification. +

+
+ Begin interview +
+
+
+ + {/* Pattern 4: Inline within a list */} +
+

4. Inline within a list

+
+
+

Assumptions

+
+
+

+ No assumptions recorded yet. They'll appear here as the interview surfaces implicit beliefs. +

+
+
+
+ + {/* Pattern 5: Attention / warning */} +
+

5. Attention — missing required section

+
+
+
+

No verification criteria

+

+ This requirement has no criteria yet. Add at least one to make the spec exportable. +

+
+
+
+
+
+ ); +}; + +// ── Shell buttons + tab switcher ───────────────────────────────────── + +export const ShellControls: Story = () => { + const [activeTab, setActiveTab] = useState('overview'); + + return ( +
+

Shell controls

+ +
+

Button variants

+
+ Ghost + Outline + Primary +
+
+ +
+

Tab switcher

+ +
+
+ ); +}; + +// ── Typography scale demo ──────────────────────────────────────────── + +export const TypographyScale: Story = () => { + return ( +
+

Typography scale

+
+

text-xxs (10px) — micro labels, counters

+

text-xs-minus (11px) — impact badges, tag labels

+

text-xs (12px) — built-in, secondary text

+

text-xs-plus (13px) — secondary body, "why" text

+

text-sm (14px) — built-in, body text

+

+ text-sm-plus (15px) — card headings, question text +

+

text-base (16px) — built-in, section headings

+
+ +
+

font-normal (400) — regular body text

+

font-medium (500) — emphasized text, labels

+

font-semibold (600) — strong emphasis

+
+ +

Color ramp

+
+
+ + ink (#202020) — primary text +
+
+ + sub (#5b5b5b) — subtitles, section headers +
+
+ + hint (#a6a6a6) — IDs, placeholders +
+
+ + rule (#e3e3e3) — borders, dividers +
+
+ + wash (#f0f0f0) — ghost fills, tracks +
+
+ + tint (#fafafa) — subtle background +
+
+ +

Shadow tokens

+
+
+

shadow-card

+
+
+

shadow-ring

+
+
+

shadow-card-ring

+
+
+
+ ); +}; diff --git a/src/client/components/app-shell.tsx b/src/client/components/app-shell.tsx new file mode 100644 index 00000000..189ae2a7 --- /dev/null +++ b/src/client/components/app-shell.tsx @@ -0,0 +1,277 @@ +import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +// ── Stage sidebar (expanded, 240px) ────────────────────────────────── + +export interface StageItem { + key: string; + label: string; + status: 'done' | 'active' | 'future'; + children?: StageItem[]; +} + +function StageIcon({ status }: { status: StageItem['status'] }) { + if (status === 'done') { + return ( + + + + ); + } + if (status === 'active') { + return ( + + + + ); + } + return ( + + + + ); +} + +function StageRow({ + stage, + depth = 0, + onClick, +}: { + stage: StageItem; + depth?: number; + onClick?: (key: string) => void; +}) { + const isTopLevel = depth === 0; + return ( + <> + + {stage.children?.map((child) => ( + + ))} + + ); +} + +export function StageSidebar({ + stages, + projectLabel = 'Project Spec', + onStageClick, + onCollapse, +}: { + stages: StageItem[]; + projectLabel?: string; + onStageClick?: (key: string) => void; + onCollapse?: () => void; +}) { + return ( + + ); +} + +// ── Phase sidebar (narrow, 48px) — collapsed view ──────────────────── + +export type Phase = 'scope' | 'design' | 'requirements' | 'criteria'; +export type PhaseStatus = 'unstarted' | 'in_progress' | 'closed'; + +const phaseOrder: Phase[] = ['scope', 'design', 'requirements', 'criteria']; + +function PhasePill({ + index, + phase, + status, + isActive, + onClick, +}: { + index: number; + phase: Phase; + status: PhaseStatus; + isActive: boolean; + onClick?: () => void; +}) { + const isFuture = status === 'unstarted'; + + return ( + + ); +} + +export function PhaseSidebar({ + phases, + activePhase, + onPhaseClick, + onExpand, +}: { + phases: Record; + activePhase: Phase; + onPhaseClick?: (phase: Phase) => void; + onExpand?: () => void; +}) { + return ( + + ); +} + +// ── Empty state card ────────────────────────────────────────────────── + +export function EmptyCard({ + title, + description, + className, + children, +}: { + title: string; + description: string; + className?: string; + children?: React.ReactNode; +}) { + return ( +
+

{title}

+

{description}

+ {children} +
+ ); +} + +// ── Shell button primitives ─────────────────────────────────────────── + +export function ShellButton({ + variant = 'ghost', + className, + ...props +}: React.ComponentProps<'button'> & { + variant?: 'ghost' | 'outline' | 'primary'; +}) { + return ( + + ); + })} + + ); +} diff --git a/src/client/index.css b/src/client/index.css index 52edf4c6..93cc6dd1 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -1,13 +1,46 @@ @import 'tailwindcss'; @import 'tw-animate-css'; @import 'shadcn/tailwind.css'; -@import '@fontsource-variable/geist'; +@import '@fontsource-variable/inter'; @custom-variant dark (&:is(.dark *)); @theme inline { --font-heading: var(--font-sans); - --font-sans: 'Geist Variable', sans-serif; + --font-sans: 'Inter Variable', sans-serif; + + /* Fine-grained text sizes matching Figma design proportions. + Tailwind v4 uses --text-* for text-* utilities. + Default gaps: xs=12px, sm=14px, base=16px. + These fill the 10–16px range needed for dense UI. */ + --text-xxs: 0.625rem; /* 10px — micro labels, counters */ + --text-xxs--line-height: 1rem; + --text-xs-minus: 0.6875rem; /* 11px — impact badges, tag labels */ + --text-xs-minus--line-height: 1rem; + /* xs = 0.75rem (12px) — built-in */ + --text-xs-plus: 0.8125rem; /* 13px — secondary body, "why" text */ + --text-xs-plus--line-height: 1.125rem; + /* sm = 0.875rem (14px) — built-in */ + --text-sm-plus: 0.9375rem; /* 15px — card headings, question text */ + --text-sm-plus--line-height: 1.375rem; + + /* Figma design grays — precise neutral ramp for borders, text, and surfaces. + Coexist with shadcn semantic names during migration. + Usage: text-ink, text-sub, text-hint, border-rule, bg-tint, bg-wash */ + --color-ink: #202020; /* primary text — almost-black */ + --color-sub: #5b5b5b; /* subtitles, section headers */ + --color-hint: #a6a6a6; /* IDs, breadcrumb inactive, placeholders */ + --color-rule: #e3e3e3; /* card borders, dividers */ + --color-wash: #f0f0f0; /* toggle tracks, ghost button fills */ + --color-tint: #fafafa; /* subtle background tint */ + + /* Shadow tokens matching Figma's consistent card shadow */ + --shadow-card: 0px 4px 4px -2px rgba(0, 0, 0, 0.02), 0px 2px 2px -1px rgba(0, 0, 0, 0.02); + --shadow-ring: 0px 0px 0px 1px rgba(0, 0, 0, 0.08); + --shadow-card-ring: + 0px 4px 4px -2px rgba(0, 0, 0, 0.02), 0px 2px 2px -1px rgba(0, 0, 0, 0.02), + 0px 0px 0px 1px rgba(0, 0, 0, 0.08); + --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -117,6 +150,11 @@ --sidebar-ring: oklch(0.556 0 0); } +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + @layer base { * { @apply border-border outline-ring/50; diff --git a/src/client/router.tsx b/src/client/router.tsx index 858341d7..f65de3ee 100644 --- a/src/client/router.tsx +++ b/src/client/router.tsx @@ -2,11 +2,15 @@ import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/re import type { ProjectListItem } from '../shared/api-types.js'; import { DebugSurfaceRouteComponent } from './routes/debug-surface.js'; +import { fetchExportPreviewLoaderData } from './routes/export-loader.js'; import { ExportPreview } from './routes/ExportPreview.js'; import { InterviewWorkspace } from './routes/InterviewWorkspace.js'; import { KnowledgeWorkspace } from './routes/KnowledgeWorkspace.js'; import { ProjectList } from './routes/ProjectList.js'; -import { fetchWorkspaceLoaderData } from './workspace/workspace-loader.js'; +import { + fetchInterviewWorkspaceLoaderData, + fetchKnowledgeWorkspaceLoaderData, +} from './workspace/workspace-loader.js'; // Root layout const rootRoute = createRootRoute({ @@ -33,7 +37,7 @@ const indexRoute = createRoute({ const projectRoute = createRoute({ getParentRoute: () => rootRoute, path: '/project/$id', - loader: async ({ params }) => fetchWorkspaceLoaderData(params.id), + loader: async ({ params }) => fetchInterviewWorkspaceLoaderData(params.id), component: InterviewWorkspace, }); @@ -41,7 +45,7 @@ const projectRoute = createRoute({ const knowledgeRoute = createRoute({ getParentRoute: () => rootRoute, path: '/project/$id/knowledge', - loader: async ({ params }) => fetchWorkspaceLoaderData(params.id), + loader: async ({ params }) => fetchKnowledgeWorkspaceLoaderData(params.id), component: KnowledgeWorkspace, }); @@ -49,6 +53,7 @@ const knowledgeRoute = createRoute({ const exportRoute = createRoute({ getParentRoute: () => rootRoute, path: '/project/$id/export', + loader: async ({ params }) => fetchExportPreviewLoaderData(params.id), component: ExportPreview, }); diff --git a/src/client/routes/ExportPreview.test.tsx b/src/client/routes/ExportPreview.test.tsx new file mode 100644 index 00000000..78216437 --- /dev/null +++ b/src/client/routes/ExportPreview.test.tsx @@ -0,0 +1,63 @@ +// @vitest-environment happy-dom + +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import type { ExportLoaderData } from './export-loader.js'; +import { ExportPreview } from './ExportPreview.js'; + +let currentLoaderData: ExportLoaderData; + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children, to, ...props }: React.AnchorHTMLAttributes & { to?: string }) => ( + + {children} + + ), + useLoaderData: () => currentLoaderData, + useParams: () => ({ id: '7' }), +})); + +vi.mock('@/components/ui/button', () => ({ + Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), +})); + +afterEach(() => { + cleanup(); + currentLoaderData = { ready: false }; +}); + +describe('ExportPreview', () => { + it('renders the blocked-export route state when the project is not ready', () => { + currentLoaderData = { ready: false }; + + render(); + + expect(screen.getByRole('heading', { name: 'Export Preview' })).toBeTruthy(); + expect( + screen.getByText('Export is not available yet. All workflow phases must be closed before exporting.'), + ).toBeTruthy(); + expect(screen.getByRole('link', { name: 'Return to interview →' })).toBeTruthy(); + }); + + it('renders markdown preview and review navigation when export data is ready', () => { + currentLoaderData = { + ready: true, + markdown: '# Reviewed Spec\n\n## Requirements\n\n- Resume from SQLite', + }; + + render(); + + expect(screen.getByRole('button', { name: 'Download .md' })).toBeTruthy(); + expect(screen.getByRole('link', { name: 'Review knowledge →' })).toBeTruthy(); + expect( + screen.getByText( + (content, element) => element?.tagName === 'PRE' && content.includes('# Reviewed Spec'), + ), + ).toBeTruthy(); + }); +}); diff --git a/src/client/routes/ExportPreview.tsx b/src/client/routes/ExportPreview.tsx index e569e1d7..c45acac9 100644 --- a/src/client/routes/ExportPreview.tsx +++ b/src/client/routes/ExportPreview.tsx @@ -1,19 +1,10 @@ -import { useQuery } from '@tanstack/react-query'; -import { Link, useParams } from '@tanstack/react-router'; +import { Link, useLoaderData, useParams } from '@tanstack/react-router'; import { Button } from '@/components/ui/button'; export function ExportPreview() { const { id } = useParams({ from: '/project/$id/export' }); - - const { data, isLoading } = useQuery({ - queryKey: ['export', id], - queryFn: async () => { - const res = await fetch(`/api/projects/${id}/export`); - if (!res.ok) throw new Error('Failed to load export'); - return res.json() as Promise<{ ready: boolean; markdown?: string }>; - }, - }); + const data = useLoaderData({ from: '/project/$id/export' }); const handleDownload = () => { if (!data?.markdown) return; @@ -33,8 +24,6 @@ export function ExportPreview() {

Export Preview

- {isLoading &&

Loading...

} - {data && !data.ready && (

diff --git a/src/client/routes/KnowledgeWorkspace.test.tsx b/src/client/routes/KnowledgeWorkspace.test.tsx index 28c6a9a1..f13068ca 100644 --- a/src/client/routes/KnowledgeWorkspace.test.tsx +++ b/src/client/routes/KnowledgeWorkspace.test.tsx @@ -2,9 +2,23 @@ import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; import type { EntitiesData } from '../../shared/api-types.js'; -import { KnowledgeWorkspaceView } from './KnowledgeWorkspace.js'; +import type { KnowledgeWorkspaceLoaderData } from '../workspace/workspace-loader.js'; +import { KnowledgeWorkspace, KnowledgeWorkspaceView } from './KnowledgeWorkspace.js'; + +let currentLoaderData: KnowledgeWorkspaceLoaderData; + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children, to, ...props }: React.AnchorHTMLAttributes & { to?: string }) => ( + + {children} + + ), + useLoaderData: () => currentLoaderData, + useParams: () => ({ id: '1' }), +})); afterEach(() => { cleanup(); @@ -22,6 +36,12 @@ const emptyEntities: EntitiesData = { relationships: [], }; +afterEach(() => { + currentLoaderData = { + entitySnapshot: emptyEntities, + }; +}); + describe('KnowledgeWorkspaceView', () => { it('renders kind-grouped sections in registry order with labels and counts', () => { const entities: EntitiesData = { @@ -115,3 +135,21 @@ describe('KnowledgeWorkspaceView', () => { expect(screen.getAllByText('Single-user only').length).toBeGreaterThanOrEqual(2); }); }); + +describe('KnowledgeWorkspace', () => { + it('renders route-level heading, navigation, and loader-backed content', () => { + currentLoaderData = { + entitySnapshot: { + ...emptyEntities, + goals: [{ id: 1, project_id: 1, kind: 'goal', subtype: null, content: 'Ship MVP', rationale: null }], + }, + }; + + render(); + + expect(screen.getByRole('heading', { name: 'Knowledge' })).toBeTruthy(); + expect(screen.getByRole('link', { name: '← Back to interview' })).toBeTruthy(); + expect(screen.getByText('Review captured knowledge items and relationships.')).toBeTruthy(); + expect(screen.getByText('Ship MVP')).toBeTruthy(); + }); +}); diff --git a/src/client/routes/KnowledgeWorkspace.tsx b/src/client/routes/KnowledgeWorkspace.tsx index 89cdeafa..5e77565c 100644 --- a/src/client/routes/KnowledgeWorkspace.tsx +++ b/src/client/routes/KnowledgeWorkspace.tsx @@ -3,7 +3,8 @@ import { Link, useLoaderData, useParams } from '@tanstack/react-router'; import { Badge } from '@/components/ui/badge'; import type { EntitiesData } from '../../shared/api-types.js'; -import { knowledgeKindRegistry, type KnowledgeCollectionKey } from '../../shared/knowledge.js'; +import { knowledgeKindRegistry } from '../../shared/knowledge.js'; +import type { KnowledgeWorkspaceLoaderData } from '../workspace/workspace-loader.js'; function entityKey(collection: string, id: number) { return `${collection}:${id}`; @@ -107,7 +108,9 @@ export function KnowledgeWorkspaceView({ entities }: { entities: EntitiesData }) export function KnowledgeWorkspace() { const { id } = useParams({ from: '/project/$id/knowledge' }); - const { entitySnapshot } = useLoaderData({ from: '/project/$id/knowledge' }); + const { entitySnapshot } = useLoaderData({ + from: '/project/$id/knowledge', + }) as KnowledgeWorkspaceLoaderData; return (

diff --git a/src/client/routes/export-loader.test.ts b/src/client/routes/export-loader.test.ts new file mode 100644 index 00000000..f6df2876 --- /dev/null +++ b/src/client/routes/export-loader.test.ts @@ -0,0 +1,33 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { fetchExportPreviewLoaderData } from './export-loader.js'; + +const fetchMock = vi.fn(); + +beforeEach(() => { + fetchMock.mockReset(); + vi.stubGlobal('fetch', fetchMock); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('export route loader', () => { + it('loads export preview data from the export endpoint', async () => { + fetchMock.mockResolvedValueOnce( + new Response(JSON.stringify({ ready: true, markdown: '# Reviewed Spec' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + await expect(fetchExportPreviewLoaderData(7)).resolves.toEqual({ + ready: true, + markdown: '# Reviewed Spec', + }); + expect(fetchMock).toHaveBeenCalledWith('/api/projects/7/export'); + }); +}); diff --git a/src/client/routes/export-loader.ts b/src/client/routes/export-loader.ts new file mode 100644 index 00000000..abb64fc8 --- /dev/null +++ b/src/client/routes/export-loader.ts @@ -0,0 +1,14 @@ +export interface ExportLoaderData { + ready: boolean; + markdown?: string; +} + +export async function fetchExportPreviewLoaderData(projectId: number | string): Promise { + const id = String(projectId); + const response = await fetch(`/api/projects/${id}/export`); + if (!response.ok) { + throw new Error('Failed to load export'); + } + + return response.json() as Promise; +} diff --git a/src/client/workspace/workspace-loader.test.ts b/src/client/workspace/workspace-loader.test.ts new file mode 100644 index 00000000..c44b80b1 --- /dev/null +++ b/src/client/workspace/workspace-loader.test.ts @@ -0,0 +1,130 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { EntitiesData, ProjectState } from '../../shared/api-types.js'; +import { fetchInterviewWorkspaceLoaderData, fetchKnowledgeWorkspaceLoaderData } from './workspace-loader.js'; + +const fetchMock = vi.fn(); + +const projectState: ProjectState = { + project: { + id: 7, + name: 'Project 7', + active_turn_id: null, + created_at: '2026-04-03 10:00:00', + updated_at: '2026-04-03 10:00:00', + }, + workflow: { + phases: { + scope: { + status: 'unstarted', + closeability: false, + readiness: 'low', + closureBasis: null, + proposalPending: false, + turnId: null, + summary: null, + }, + design: { + status: 'unstarted', + closeability: false, + readiness: 'low', + closureBasis: null, + proposalPending: false, + turnId: null, + summary: null, + }, + requirements: { + status: 'unstarted', + closeability: false, + readiness: 'low', + closureBasis: null, + proposalPending: false, + turnId: null, + summary: null, + }, + criteria: { + status: 'unstarted', + closeability: false, + readiness: 'low', + closureBasis: null, + proposalPending: false, + turnId: null, + summary: null, + }, + }, + }, + turns: [], +}; + +const entitySnapshot: EntitiesData = { + goals: [], + terms: [], + contexts: [], + constraints: [], + requirements: [], + criteria: [], + decisions: [], + assumptions: [], + relationships: [], +}; + +beforeEach(() => { + fetchMock.mockReset(); + vi.stubGlobal('fetch', fetchMock); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('workspace route loaders', () => { + it('loads interview workspace route data from the project and entities endpoints', async () => { + fetchMock + .mockResolvedValueOnce( + new Response(JSON.stringify(projectState), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(entitySnapshot), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + await expect(fetchInterviewWorkspaceLoaderData(7)).resolves.toEqual({ projectState, entitySnapshot }); + expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/projects/7'); + expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/projects/7/entities'); + }); + + it('loads knowledge workspace route data from the same current route contract through its own helper', async () => { + fetchMock + .mockResolvedValueOnce( + new Response(JSON.stringify(projectState), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(entitySnapshot), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + await expect(fetchKnowledgeWorkspaceLoaderData('7')).resolves.toEqual({ entitySnapshot }); + expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/projects/7'); + expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/projects/7/entities'); + }); + + it('fails knowledge workspace loading when the project does not exist', async () => { + fetchMock.mockResolvedValueOnce(new Response('Not found', { status: 404 })); + + await expect(fetchKnowledgeWorkspaceLoaderData('999')).rejects.toThrow('Failed to load project'); + expect(fetchMock).toHaveBeenCalledOnce(); + expect(fetchMock).toHaveBeenCalledWith('/api/projects/999'); + }); +}); diff --git a/src/client/workspace/workspace-loader.ts b/src/client/workspace/workspace-loader.ts index b5ded259..aa3318d2 100644 --- a/src/client/workspace/workspace-loader.ts +++ b/src/client/workspace/workspace-loader.ts @@ -5,6 +5,10 @@ export interface WorkspaceLoaderData { entitySnapshot: EntitiesData; } +export interface KnowledgeWorkspaceLoaderData { + entitySnapshot: EntitiesData; +} + async function fetchJson(url: string, errorMessage: string): Promise { const response = await fetch(url); if (!response.ok) { @@ -14,7 +18,7 @@ async function fetchJson(url: string, errorMessage: string): Promise { return response.json() as Promise; } -export async function fetchWorkspaceLoaderData(projectId: number | string): Promise { +async function fetchWorkflowDetailLoaderData(projectId: number | string): Promise { const id = String(projectId); const [projectState, entitySnapshot] = await Promise.all([ fetchJson(`/api/projects/${id}`, 'Failed to load project'), @@ -23,3 +27,22 @@ export async function fetchWorkspaceLoaderData(projectId: number | string): Prom return { projectState, entitySnapshot }; } + +export async function fetchInterviewWorkspaceLoaderData( + projectId: number | string, +): Promise { + return fetchWorkflowDetailLoaderData(projectId); +} + +export async function fetchKnowledgeWorkspaceLoaderData( + projectId: number | string, +): Promise { + const id = String(projectId); + await fetchJson(`/api/projects/${id}`, 'Failed to load project'); + const entitySnapshot = await fetchJson( + `/api/projects/${id}/entities`, + 'Failed to load project entities', + ); + + return { entitySnapshot }; +} diff --git a/src/server/app.test.ts b/src/server/app.test.ts index 13f31a11..76f5c693 100644 --- a/src/server/app.test.ts +++ b/src/server/app.test.ts @@ -275,7 +275,9 @@ describe('GET /api/projects/:id/export', () => { const res = await request(app).get(`/api/projects/${projectId}/export`).expect(200); expect(res.body.ready).toBe(true); expect(res.body.markdown).toContain('# Done'); + expect(res.body.markdown).toContain('Resume the interview from SQLite after restart'); expect(res.body.markdown).toContain('Verify SQLite resume'); + expect(res.body.markdown).not.toContain('Support exporting the spec as a PDF'); }); }); diff --git a/src/server/app.ts b/src/server/app.ts index 5198acf9..091f9f9b 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -39,6 +39,7 @@ import { getOptionsForTurn, updateTurn, getEntitiesForProject, + getEntitiesForProjectOnActivePath, recordReviewFromTurnResponse, } from './db.js'; import { isExportReady, renderExportMarkdown } from './export.js'; @@ -169,7 +170,7 @@ export function createApp(dbPath?: string) { res.json({ ready: false }); return; } - const entities = getEntitiesForProject(db, id); + const entities = getEntitiesForProjectOnActivePath(db, id); const markdown = renderExportMarkdown(projectState.project.name, entities, projectState.workflow); res.json({ ready: true, markdown }); }); diff --git a/src/server/db.ts b/src/server/db.ts index 2a136110..9624dba9 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -792,6 +792,27 @@ export function getScopeBundleForProject(db: DB, projectId: number) { }; } +function getKnowledgeItemIdsLinkedToActivePath(db: DB, projectId: number): Set { + const activeTurnIds = getActivePath(db, projectId).map((turn) => turn.id); + if (activeTurnIds.length === 0) { + return new Set(); + } + + const rows = db + .select({ itemId: schema.turnKnowledgeItem.item_id }) + .from(schema.turnKnowledgeItem) + .innerJoin(schema.knowledgeItem, eq(schema.knowledgeItem.id, schema.turnKnowledgeItem.item_id)) + .where( + and( + eq(schema.knowledgeItem.project_id, projectId), + inArray(schema.turnKnowledgeItem.turn_id, activeTurnIds), + ), + ) + .all() as Array<{ itemId: number }>; + + return new Set(rows.map((row) => row.itemId)); +} + export function getEntitiesForProject(db: DB, projectId: number): EntitiesForProject { const genericKnowledgeCollections = Object.fromEntries( genericKnowledgeKindRegistry.map((entry) => [ @@ -851,3 +872,23 @@ export function getEntitiesForProject(db: DB, projectId: number): EntitiesForPro })), }; } + +export function getEntitiesForProjectOnActivePath(db: DB, projectId: number): EntitiesForProject { + const entities = getEntitiesForProject(db, projectId); + const activeItemIds = getKnowledgeItemIdsLinkedToActivePath(db, projectId); + + return { + goals: entities.goals.filter((item) => activeItemIds.has(item.id)), + terms: entities.terms.filter((item) => activeItemIds.has(item.id)), + contexts: entities.contexts.filter((item) => activeItemIds.has(item.id)), + constraints: entities.constraints.filter((item) => activeItemIds.has(item.id)), + requirements: entities.requirements.filter((item) => activeItemIds.has(item.id)), + criteria: entities.criteria.filter((item) => activeItemIds.has(item.id)), + decisions: entities.decisions.filter((item) => activeItemIds.has(item.id)), + assumptions: entities.assumptions.filter((item) => activeItemIds.has(item.id)), + relationships: entities.relationships.filter( + (relationship) => + activeItemIds.has(relationship.source.id) && activeItemIds.has(relationship.target.id), + ), + }; +} diff --git a/src/server/export.test.ts b/src/server/export.test.ts index 0ee20943..93c4763a 100644 --- a/src/server/export.test.ts +++ b/src/server/export.test.ts @@ -1,14 +1,35 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; import type { EntitiesData } from '../shared/api-types.js'; -import type { WorkflowState } from './db.js'; -import { renderExportMarkdown } from './export.js'; +import { getProjectState } from './core.js'; +import { + advanceHead, + createDb, + createKnowledgeItem, + createProject, + createTurn, + getEntitiesForProject, + getEntitiesForProjectOnActivePath, + linkKnowledgeItemToTurn, + type WorkflowState, +} from './db.js'; +import { buildReviewedExportProjection, renderExportMarkdown } from './export.js'; +import { + seedAllPhasesClosedWithForcedDesign, + seedAllPhasesClosedWithLowReadinessScope, +} from './fixtures/scenarios.js'; -function createClosedPhase(basis: string = 'interviewer_recommended') { +function createClosedPhase({ + basis = 'interviewer_recommended', + readiness = 'high', +}: { + basis?: string; + readiness?: 'low' | 'medium' | 'high'; +} = {}) { return { status: 'closed' as const, closeability: true, - readiness: 'high' as const, + readiness, closureBasis: basis, proposalPending: false, turnId: 1, @@ -41,6 +62,58 @@ const emptyEntities: EntitiesData = { }; describe('renderExportMarkdown', () => { + const openDbs: Array> = []; + + afterEach(() => { + while (openDbs.length > 0) { + openDbs.pop()?.$client.close(); + } + }); + + it('projects reviewed export sections and caveats before markdown rendering', () => { + const entities: EntitiesData = { + ...emptyEntities, + requirements: [ + { + id: 1, + project_id: 1, + kind: 'requirement', + subtype: null, + content: 'Export spec', + rationale: null, + reviewStatus: 'approved', + }, + { + id: 2, + project_id: 1, + kind: 'requirement', + subtype: null, + content: 'PDF export', + rationale: null, + reviewStatus: 'rejected', + }, + ], + decisions: [{ id: 3, project_id: 1, content: 'Use SQLite', rationale: 'Zero config' }], + }; + const workflow = createAllClosedWorkflow({ + design: createClosedPhase({ basis: 'user_forced', readiness: 'low' }), + }); + + expect(buildReviewedExportProjection(entities, workflow)).toEqual({ + caveats: ['design was closed via user-forced closure', 'design was closed with low readiness'], + sections: [ + { + heading: 'Requirements', + items: [{ content: 'Export spec', rationale: null }], + }, + { + heading: 'Decisions', + items: [{ content: 'Use SQLite', rationale: 'Zero config' }], + }, + ], + }); + }); + it('renders kind-grouped sections from entities', () => { const entities: EntitiesData = { ...emptyEntities, @@ -85,7 +158,7 @@ describe('renderExportMarkdown', () => { it('includes closure caveats for forced-close phases', () => { const workflow = createAllClosedWorkflow({ - design: createClosedPhase('user_forced'), + design: createClosedPhase({ basis: 'user_forced' }), }); const md = renderExportMarkdown('Test', emptyEntities, workflow); @@ -94,7 +167,7 @@ describe('renderExportMarkdown', () => { expect(md).toContain('user-forced'); }); - it('includes review status for requirements and criteria', () => { + it('renders only approved reviewed items in the export body', () => { const entities: EntitiesData = { ...emptyEntities, requirements: [ @@ -116,12 +189,151 @@ describe('renderExportMarkdown', () => { rationale: null, reviewStatus: 'rejected', }, + { + id: 3, + project_id: 1, + kind: 'requirement', + subtype: null, + content: 'CSV export', + rationale: null, + reviewStatus: 'pending', + }, + ], + criteria: [ + { + id: 4, + project_id: 1, + kind: 'criterion', + subtype: null, + content: 'Reload shows the active interview state', + rationale: null, + reviewStatus: 'approved', + }, + { + id: 5, + project_id: 1, + kind: 'criterion', + subtype: null, + content: 'PDF download works offline', + rationale: null, + reviewStatus: 'rejected', + }, ], }; const md = renderExportMarkdown('Test', entities, createAllClosedWorkflow()); - expect(md).toMatch(/Export spec.*approved/i); - expect(md).toMatch(/PDF export.*rejected/i); + expect(md).toContain('Export spec'); + expect(md).toContain('Reload shows the active interview state'); + expect(md).not.toContain('PDF export'); + expect(md).not.toContain('CSV export'); + expect(md).not.toContain('PDF download works offline'); + expect(md).not.toMatch(/\bapproved\b/i); + expect(md).not.toMatch(/\brejected\b/i); + }); + + it('includes closure caveats for low-readiness phases', () => { + const workflow = createAllClosedWorkflow({ + design: createClosedPhase({ readiness: 'low' }), + }); + + const md = renderExportMarkdown('Test', emptyEntities, workflow); + + expect(md).toContain('design'); + expect(md).toContain('low readiness'); + }); + + it('filters export content to knowledge linked on the active path', () => { + const db = createDb(); + openDbs.push(db); + const project = createProject(db, 'Branching Project'); + const rootTurn = createTurn(db, project.id, { + phase: 'scope', + question: 'What database?', + answer: 'We are still deciding.', + }); + const abandonedBranchTurn = createTurn(db, project.id, { + phase: 'design', + parent_turn_id: rootTurn.id, + question: 'Which storage option?', + answer: 'Take the SQLite branch.', + }); + const activeBranchTurn = createTurn(db, project.id, { + phase: 'design', + parent_turn_id: rootTurn.id, + question: 'Which storage option?', + answer: 'Take the Postgres branch.', + }); + advanceHead(db, project.id, activeBranchTurn.id); + + const abandonedDecision = createKnowledgeItem(db, project.id, 'decision', 'Use SQLite for persistence', { + rationale: 'This belonged to the abandoned branch.', + }); + const activeDecision = createKnowledgeItem(db, project.id, 'decision', 'Use Postgres for persistence', { + rationale: 'This belongs to the active branch.', + }); + linkKnowledgeItemToTurn(db, abandonedDecision.id, abandonedBranchTurn.id); + linkKnowledgeItemToTurn(db, activeDecision.id, activeBranchTurn.id); + + expect(getEntitiesForProject(db, project.id).decisions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ content: 'Use SQLite for persistence' }), + expect.objectContaining({ content: 'Use Postgres for persistence' }), + ]), + ); + + const markdown = renderExportMarkdown( + project.name, + getEntitiesForProjectOnActivePath(db, project.id), + createAllClosedWorkflow(), + ); + + expect(markdown).toContain('Use Postgres for persistence'); + expect(markdown).not.toContain('Use SQLite for persistence'); + }); + + it('renders the forced-close canonical fixture with the expected export caveat', () => { + const db = createDb(); + openDbs.push(db); + const projectId = createProject(db, 'Forced-Close All Phases Closed').id; + seedAllPhasesClosedWithForcedDesign(db, projectId); + + const projectState = getProjectState(db, projectId); + expect(projectState).not.toBeNull(); + expect(projectState?.workflow.phases.design).toMatchObject({ + status: 'closed', + closureBasis: 'user_forced', + }); + + const markdown = renderExportMarkdown( + projectState!.project.name, + getEntitiesForProject(db, projectId), + projectState!.workflow, + ); + expect(markdown).toContain('__design__ was closed via user-forced closure'); + expect(markdown).not.toContain('Support exporting the spec as a PDF'); + }); + + it('renders the low-readiness canonical fixture with the expected export caveat', () => { + const db = createDb(); + openDbs.push(db); + const projectId = createProject(db, 'Low-Readiness All Phases Closed').id; + seedAllPhasesClosedWithLowReadinessScope(db, projectId); + + const projectState = getProjectState(db, projectId); + expect(projectState).not.toBeNull(); + expect(projectState?.workflow.phases.scope).toMatchObject({ + status: 'closed', + readiness: 'low', + closureBasis: 'interviewer_recommended', + }); + + const markdown = renderExportMarkdown( + projectState!.project.name, + getEntitiesForProject(db, projectId), + projectState!.workflow, + ); + expect(markdown).toContain('__scope__ was closed with low readiness'); + expect(markdown).not.toContain('Support exporting the spec as a PDF'); }); }); diff --git a/src/server/export.ts b/src/server/export.ts index 8f0d8ac5..43c5aba2 100644 --- a/src/server/export.ts +++ b/src/server/export.ts @@ -4,26 +4,75 @@ import type { EntitiesData } from '../shared/api-types.js'; import { knowledgeKindRegistry } from '../shared/knowledge.js'; import type { WorkflowState } from './db.js'; -function renderItem(item: { content: string; rationale?: string | null; reviewStatus?: string }): string { +export interface ReviewedExportItem { + content: string; + rationale?: string | null; +} + +export interface ReviewedExportSection { + heading: string; + items: ReviewedExportItem[]; +} + +export interface ReviewedExportProjection { + caveats: string[]; + sections: ReviewedExportSection[]; +} + +function renderItem(item: ReviewedExportItem): string { const parts = [item.content]; - if (item.reviewStatus) { - parts.push(`(${item.reviewStatus})`); - } if (item.rationale) { parts.push(`— ${item.rationale}`); } return parts.join(' '); } -function renderCaveats(workflow: WorkflowState): string { +function getReviewedExportItems( + items: Array<{ content: string; rationale?: string | null; reviewStatus?: string }>, +) { + return items.filter((item) => !('reviewStatus' in item) || item.reviewStatus === 'approved'); +} + +function getReviewedExportCaveats(workflow: WorkflowState): string[] { const caveats: string[] = []; for (const [phase, state] of Object.entries(workflow.phases)) { if (state.closureBasis && state.closureBasis !== 'interviewer_recommended') { - caveats.push(`${bold(phase)} was closed via user-forced closure`); + caveats.push(`${phase} was closed via user-forced closure`); + } + if (state.readiness === 'low') { + caveats.push(`${phase} was closed with low readiness`); } } + return caveats; +} + +function renderCaveats(caveats: string[]): string { if (caveats.length === 0) return ''; - return `${h3('Closure Caveats')}\n\n${ul(caveats)}\n`; + return `${h3('Closure Caveats')}\n\n${ul(caveats.map((caveat) => bold(caveat.split(' ')[0]!) + caveat.slice(caveat.indexOf(' '))))}\n`; +} + +export function buildReviewedExportProjection( + entities: EntitiesData, + workflow: WorkflowState, +): ReviewedExportProjection { + return { + caveats: getReviewedExportCaveats(workflow), + sections: knowledgeKindRegistry.flatMap((entry) => { + const items = getReviewedExportItems(entities[entry.collectionKey]).map((item) => ({ + content: item.content, + rationale: item.rationale, + })); + + return items.length > 0 + ? [ + { + heading: entry.label, + items, + } satisfies ReviewedExportSection, + ] + : []; + }), + }; } export function renderExportMarkdown( @@ -32,19 +81,17 @@ export function renderExportMarkdown( workflow: WorkflowState, ): string { const sections: string[] = [h1(projectName), '']; + const projection = buildReviewedExportProjection(entities, workflow); - const caveatSection = renderCaveats(workflow); + const caveatSection = renderCaveats(projection.caveats); if (caveatSection) { sections.push(caveatSection); } - for (const entry of knowledgeKindRegistry) { - const items = entities[entry.collectionKey]; - if (items.length === 0) continue; - - sections.push(h2(entry.label)); + for (const section of projection.sections) { + sections.push(h2(section.heading)); sections.push(''); - sections.push(ul(items.map(renderItem))); + sections.push(ul(section.items.map(renderItem))); sections.push(''); } diff --git a/src/server/fixtures/manifest.test.ts b/src/server/fixtures/manifest.test.ts new file mode 100644 index 00000000..ecb8106e --- /dev/null +++ b/src/server/fixtures/manifest.test.ts @@ -0,0 +1,51 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { loadActivePathWithOptions } from '../core.js'; +import { createDb, type DB } from '../db.js'; +import { formatProjectedTurnResponse, projectTurnResponse } from '../turn-response.js'; +import { seedFromManifest, type ManifestScenario } from './manifest.js'; + +let db: DB; + +beforeEach(() => { + db = createDb(); +}); + +afterEach(() => { + db.$client.close(); +}); + +describe('seedFromManifest', () => { + it('persists selected option ids in user_parts so seeded turns rehydrate with option text', () => { + const scenario: ManifestScenario = { + turns: [ + { + phase: 'scope', + question: 'Which launch surface should we prioritize?', + why: 'The fixture should preserve the selected option text after reload.', + impact: 'high', + answer: 'Start with the web workspace.', + options: [ + { content: 'CLI-first workflow', is_recommended: false }, + { content: 'Web workspace', is_recommended: true }, + ], + selectedOptionPositions: [1], + }, + ], + knowledgeItems: [], + edges: [], + }; + + const projectId = seedFromManifest(db, scenario, 'Manifest Seed'); + const turn = loadActivePathWithOptions(db, projectId)[0]!; + const projectedResponse = projectTurnResponse(turn); + + expect(projectedResponse).toEqual({ + selectedOptionIds: [turn.options![1]!.id], + selectedOptionContents: ['Web workspace'], + freeText: undefined, + }); + expect(formatProjectedTurnResponse(projectedResponse!)).toContain('Chosen options: Web workspace'); + expect(formatProjectedTurnResponse(projectedResponse!)).not.toContain('Chosen options: 1'); + }); +}); diff --git a/src/server/fixtures/manifest.ts b/src/server/fixtures/manifest.ts new file mode 100644 index 00000000..69676c61 --- /dev/null +++ b/src/server/fixtures/manifest.ts @@ -0,0 +1,305 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { and, eq } from 'drizzle-orm'; + +import { + advanceHead, + confirmPhaseOutcome, + createKnowledgeItem, + createOption, + createPhaseOutcome, + createProject, + createTurn, + linkKnowledgeItemToTurn, + applyTurnResponseSelections, + type DB, +} from '../db.js'; +import * as schema from '../schema.js'; +import type { ScenarioFn } from './scenarios.js'; + +// --------------------------------------------------------------------------- +// Manifest types +// --------------------------------------------------------------------------- + +export interface ManifestOption { + content: string; + is_recommended: boolean; +} + +export interface ManifestTurn { + phase: 'scope' | 'design' | 'requirements' | 'criteria'; + question: string; + answer: string; + why?: string | null; + impact?: 'high' | 'medium' | 'low' | null; + options?: ManifestOption[]; + selectedOptionPositions?: number[]; + freeText?: string | null; + isProposal?: boolean; + isConfirmation?: boolean; +} + +export interface ManifestKnowledgeItem { + kind: 'goal' | 'term' | 'context' | 'constraint' | 'decision' | 'assumption' | 'requirement' | 'criterion'; + content: string; + rationale?: string | null; + capturedAtTurn: number; + reviewAction?: 'reviewed' | 'rejected'; + reviewedAtTurn?: number; +} + +export interface ManifestEdge { + fromItemIndex: number; + toItemIndex: number; + relation: 'depends_on' | 'derived_from' | 'constrains' | 'verifies' | 'refines'; +} + +export interface ManifestScenario { + turns: ManifestTurn[]; + knowledgeItems: ManifestKnowledgeItem[]; + edges: ManifestEdge[]; +} + +export interface Manifest { + name: string; + description: string; + scenarios: Record; +} + +// --------------------------------------------------------------------------- +// Seeder +// --------------------------------------------------------------------------- + +export function seedFromManifest(db: DB, scenario: ManifestScenario, projectName: string): number { + const project = createProject(db, projectName); + const projectId = project.id; + + // Track manifest turn index → actual turn ID + const turnIdMap = new Map(); + let prevTurnId: number | null = null; + + for (let i = 0; i < scenario.turns.length; i++) { + const mt = scenario.turns[i]!; + + if (mt.isConfirmation) { + // Find the most recent proposal turn in the same phase + let proposalTurnId: number | null = null; + for (let j = i - 1; j >= 0; j--) { + if (scenario.turns[j]!.isProposal && scenario.turns[j]!.phase === mt.phase) { + proposalTurnId = turnIdMap.get(j) ?? null; + break; + } + } + + const userParts = JSON.stringify([ + { type: 'text', text: `Confirm ${mt.phase} closure` }, + { + type: 'data-confirmation', + data: { + kind: 'confirm-proposed-phase-closure', + proposalTurnId, + phase: mt.phase, + }, + }, + ]); + + const turn = createTurn(db, projectId, { + phase: mt.phase, + parent_turn_id: prevTurnId, + question: '', + answer: `Confirm ${mt.phase} closure`, + user_parts: userParts, + }); + turnIdMap.set(i, turn.id); + + // Find the phase outcome created by the proposal turn and confirm it + const outcome = + proposalTurnId != null + ? (db + .select() + .from(schema.phaseOutcome) + .where( + and( + eq(schema.phaseOutcome.project_id, projectId), + eq(schema.phaseOutcome.proposal_turn_id, proposalTurnId), + eq(schema.phaseOutcome.status, 'proposed'), + ), + ) + .get() as { id: number } | undefined) + : undefined; + if (outcome) { + confirmPhaseOutcome(db, outcome.id, turn.id); + } + + advanceHead(db, projectId, turn.id); + prevTurnId = turn.id; + continue; + } + + if (mt.isProposal) { + const assistantParts = JSON.stringify([ + { type: 'text', text: '' }, + { + type: 'tool-propose_phase_closure', + toolCallId: `tc_proposal_${i}`, + state: 'output-available', + input: { phase: mt.phase, summary: mt.answer }, + output: { ok: true, turnId: -1, phase: mt.phase }, // placeholder, updated below + }, + ]); + + const turn = createTurn(db, projectId, { + phase: mt.phase, + parent_turn_id: prevTurnId, + question: '', + answer: mt.answer, + assistant_parts: assistantParts, + }); + turnIdMap.set(i, turn.id); + + createPhaseOutcome(db, { + projectId, + phase: mt.phase, + proposal_turn_id: turn.id, + summary: mt.answer, + }); + + advanceHead(db, projectId, turn.id); + prevTurnId = turn.id; + continue; + } + + // Regular turn + const options = mt.options ?? []; + const assistantParts = JSON.stringify([ + { type: 'text', text: '' }, + { + type: 'tool-ask_question', + toolCallId: `tc_${i}`, + state: 'output-available', + input: { + question: mt.question, + why: mt.why ?? null, + impact: mt.impact ?? null, + options, + }, + output: { ok: true, turnId: -1, optionCount: options.length }, + }, + ]); + + // We need the turn ID for user_parts, so create first then update user_parts + const turn = createTurn(db, projectId, { + phase: mt.phase, + parent_turn_id: prevTurnId, + question: mt.question, + why: mt.why ?? null, + impact: mt.impact ?? null, + answer: mt.answer, + assistant_parts: assistantParts, + }); + turnIdMap.set(i, turn.id); + + // Create options in DB and retain their row IDs for user_parts rehydration. + const optionIdsByPosition = new Map(); + for (let p = 0; p < options.length; p++) { + const opt = options[p]!; + const createdOption = createOption(db, turn.id, { + position: p, + content: opt.content, + is_recommended: opt.is_recommended, + }); + optionIdsByPosition.set(p, createdOption.id); + } + + // Apply selections + if (mt.selectedOptionPositions && mt.selectedOptionPositions.length > 0) { + applyTurnResponseSelections(db, turn.id, mt.selectedOptionPositions); + } + + // Set user_parts (needs actual turn.id, so done after creation) + const selectedIds = (mt.selectedOptionPositions ?? []) + .map((position) => optionIdsByPosition.get(position)) + .filter((optionId): optionId is number => optionId != null); + const userParts = JSON.stringify([ + { type: 'text', text: mt.answer }, + { + type: 'data-turn-response', + data: { + turnId: turn.id, + selectedOptionIds: selectedIds, + freeText: mt.freeText ?? undefined, + }, + }, + ]); + db.update(schema.turn).set({ user_parts: userParts }).where(eq(schema.turn.id, turn.id)).run(); + + advanceHead(db, projectId, turn.id); + prevTurnId = turn.id; + } + + // --- Knowledge items --- + const itemIdMap = new Map(); + + for (let k = 0; k < scenario.knowledgeItems.length; k++) { + const mi = scenario.knowledgeItems[k]!; + const item = createKnowledgeItem(db, projectId, mi.kind, mi.content, { + rationale: mi.rationale ?? null, + }); + itemIdMap.set(k, item.id); + + // Link to capturing turn + const captureTurnId = turnIdMap.get(mi.capturedAtTurn); + if (captureTurnId != null) { + linkKnowledgeItemToTurn(db, item.id, captureTurnId, 'captured'); + } + + // Link review action + if (mi.reviewAction && mi.reviewedAtTurn != null) { + const reviewTurnId = turnIdMap.get(mi.reviewedAtTurn); + if (reviewTurnId != null) { + linkKnowledgeItemToTurn(db, item.id, reviewTurnId, mi.reviewAction); + } + } + } + + // --- Edges --- + for (const edge of scenario.edges) { + const fromId = itemIdMap.get(edge.fromItemIndex); + const toId = itemIdMap.get(edge.toItemIndex); + if (fromId != null && toId != null) { + db.insert(schema.knowledgeEdge) + .values({ from_item_id: fromId, to_item_id: toId, relation: edge.relation }) + .run(); + } + } + + return projectId; +} + +// --------------------------------------------------------------------------- +// Manifest loader +// --------------------------------------------------------------------------- + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export function loadManifestScenarios(manifestName: string): Record { + const filePath = join(__dirname, 'manifests', `${manifestName}.json`); + const raw = readFileSync(filePath, 'utf-8'); + const manifest: Manifest = JSON.parse(raw); + + const result: Record = {}; + + for (const scenarioKey of Object.keys(manifest.scenarios)) { + const scenario = manifest.scenarios[scenarioKey]!; + const fullKey = `${manifestName}-${scenarioKey}`; + const defaultName = `${manifest.name} (${scenarioKey})`; + + result[fullKey] = (db: DB, projectName?: string) => { + return seedFromManifest(db, scenario, projectName ?? defaultName); + }; + } + + return result; +} diff --git a/src/server/fixtures/manifests/issue-tracker.json b/src/server/fixtures/manifests/issue-tracker.json new file mode 100644 index 00000000..50f07c2d --- /dev/null +++ b/src/server/fixtures/manifests/issue-tracker.json @@ -0,0 +1,2218 @@ +{ + "name": "Issue Tracker", + "description": "A tiny issue tracker for a small software team with tickets, comments, status changes, audit history, and role-based permissions.", + "scenarios": { + "scope-closed": { + "turns": [ + { + "phase": "scope", + "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?", + "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.", + "impact": "high", + "answer": "Replace our spreadsheet — we need proper tracking from creation to completion", + "options": [ + { + "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker", + "is_recommended": true + }, + { + "content": "Introduce issue tracking as a new practice — the team currently has no formal process", + "is_recommended": false + }, + { + "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability" + }, + { + "phase": "scope", + "question": "Who are the primary users of this system, and how large is the team?", + "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.", + "impact": "high", + "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility", + "options": [ + { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true }, + { + "content": "A medium team (10-30 people) with multiple squads and project leads", + "is_recommended": false + }, + { + "content": "A cross-functional group including developers, QA, product managers, and stakeholders", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards" + }, + { + "phase": "scope", + "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.", + "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.", + "impact": "high", + "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit", + "options": [ + { + "content": "Tickets (work items) with status, assignee, priority, and description", + "is_recommended": true + }, + { + "content": "Tickets plus epics or parent groupings for organizing related work", + "is_recommended": false + }, + { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates" + }, + { + "phase": "scope", + "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.", + "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.", + "impact": "high", + "answer": "Yes, we need full audit history for compliance — every status change must be traceable", + "options": [ + { + "content": "Full audit trail required — every status change must record who, what, and when", + "is_recommended": true + }, + { + "content": "Basic change history is nice to have but not a compliance requirement", + "is_recommended": false + }, + { + "content": "No audit requirements — simple current-state tracking is sufficient", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "scope", + "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.", + "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.", + "impact": "medium", + "answer": "Keep it simpler than Jira, add role-based permissions with three roles", + "options": [ + { + "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1", + "is_recommended": true + }, + { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false }, + { + "content": "Build a minimal viable tracker and expand based on user feedback", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We need role-based permissions though — admin, developer, and viewer roles" + }, + { + "phase": "scope", + "question": "", + "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.", + "isProposal": true + }, + { + "phase": "scope", + "question": "", + "answer": "Confirm scope closure", + "isConfirmation": true + } + ], + "knowledgeItems": [ + { + "kind": "goal", + "content": "Track work items from creation to completion with clear ownership and status visibility", + "capturedAtTurn": 0 + }, + { + "kind": "goal", + "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements", + "capturedAtTurn": 3 + }, + { + "kind": "goal", + "content": "Enable team-wide visibility into work distribution and project progress", + "capturedAtTurn": 1 + }, + { + "kind": "term", + "content": "ticket — A trackable unit of work with status, assignee, priority, and description", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "assignee — Team member responsible for completing a ticket", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "comment — Timestamped note attached to a ticket for discussion and status updates", + "capturedAtTurn": 2 + }, + { + "kind": "context", + "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility", + "capturedAtTurn": 1 + }, + { + "kind": "context", + "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability", + "capturedAtTurn": 0 + }, + { + "kind": "constraint", + "content": "Must be simpler than Jira — no custom workflows or custom fields in v1", + "capturedAtTurn": 4 + }, + { + "kind": "constraint", + "content": "Audit history required for compliance — every status change must be traceable", + "capturedAtTurn": 3 + }, + { + "kind": "constraint", + "content": "Role-based permissions with three roles: admin, developer, viewer", + "capturedAtTurn": 4 + } + ], + "edges": [ + { + "fromItemIndex": 10, + "toItemIndex": 1, + "relation": "constrains" + }, + { + "fromItemIndex": 8, + "toItemIndex": 2, + "relation": "derived_from" + }, + { + "fromItemIndex": 9, + "toItemIndex": 0, + "relation": "constrains" + } + ] + }, + "design-active": { + "turns": [ + { + "phase": "scope", + "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?", + "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.", + "impact": "high", + "answer": "Replace our spreadsheet — we need proper tracking from creation to completion", + "options": [ + { + "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker", + "is_recommended": true + }, + { + "content": "Introduce issue tracking as a new practice — the team currently has no formal process", + "is_recommended": false + }, + { + "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability" + }, + { + "phase": "scope", + "question": "Who are the primary users of this system, and how large is the team?", + "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.", + "impact": "high", + "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility", + "options": [ + { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true }, + { + "content": "A medium team (10-30 people) with multiple squads and project leads", + "is_recommended": false + }, + { + "content": "A cross-functional group including developers, QA, product managers, and stakeholders", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards" + }, + { + "phase": "scope", + "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.", + "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.", + "impact": "high", + "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit", + "options": [ + { + "content": "Tickets (work items) with status, assignee, priority, and description", + "is_recommended": true + }, + { + "content": "Tickets plus epics or parent groupings for organizing related work", + "is_recommended": false + }, + { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates" + }, + { + "phase": "scope", + "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.", + "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.", + "impact": "high", + "answer": "Yes, we need full audit history for compliance — every status change must be traceable", + "options": [ + { + "content": "Full audit trail required — every status change must record who, what, and when", + "is_recommended": true + }, + { + "content": "Basic change history is nice to have but not a compliance requirement", + "is_recommended": false + }, + { + "content": "No audit requirements — simple current-state tracking is sufficient", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "scope", + "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.", + "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.", + "impact": "medium", + "answer": "Keep it simpler than Jira, add role-based permissions with three roles", + "options": [ + { + "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1", + "is_recommended": true + }, + { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false }, + { + "content": "Build a minimal viable tracker and expand based on user feedback", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We need role-based permissions though — admin, developer, and viewer roles" + }, + { + "phase": "scope", + "question": "", + "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.", + "isProposal": true + }, + { + "phase": "scope", + "question": "", + "answer": "Confirm scope closure", + "isConfirmation": true + }, + { + "phase": "design", + "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?", + "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.", + "impact": "high", + "answer": "Kanban board with swimlanes per assignee — that's how we think about work", + "options": [ + { + "content": "Kanban board with columns per status and optional swimlanes per assignee", + "is_recommended": true + }, + { + "content": "Sortable table view with inline editing and bulk actions", + "is_recommended": false + }, + { + "content": "Combined view with a board as default and table as an alternative", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "design", + "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.", + "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.", + "impact": "high", + "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket", + "options": [ + { + "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline", + "is_recommended": true + }, + { + "content": "Separate audit table with structured event records for each field change", + "is_recommended": false + }, + { + "content": "Event-sourced model where the ticket state is derived from an append-only event log", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket" + } + ], + "knowledgeItems": [ + { + "kind": "goal", + "content": "Track work items from creation to completion with clear ownership and status visibility", + "capturedAtTurn": 0 + }, + { + "kind": "goal", + "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements", + "capturedAtTurn": 3 + }, + { + "kind": "goal", + "content": "Enable team-wide visibility into work distribution and project progress", + "capturedAtTurn": 1 + }, + { + "kind": "term", + "content": "ticket — A trackable unit of work with status, assignee, priority, and description", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "assignee — Team member responsible for completing a ticket", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "comment — Timestamped note attached to a ticket for discussion and status updates", + "capturedAtTurn": 2 + }, + { + "kind": "context", + "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility", + "capturedAtTurn": 1 + }, + { + "kind": "context", + "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability", + "capturedAtTurn": 0 + }, + { + "kind": "constraint", + "content": "Must be simpler than Jira — no custom workflows or custom fields in v1", + "capturedAtTurn": 4 + }, + { + "kind": "constraint", + "content": "Audit history required for compliance — every status change must be traceable", + "capturedAtTurn": 3 + }, + { + "kind": "constraint", + "content": "Role-based permissions with three roles: admin, developer, viewer", + "capturedAtTurn": 4 + }, + { + "kind": "decision", + "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee", + "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering", + "capturedAtTurn": 7 + }, + { + "kind": "decision", + "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline", + "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth", + "capturedAtTurn": 8 + }, + { + "kind": "decision", + "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)", + "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed", + "capturedAtTurn": 7 + }, + { + "kind": "assumption", + "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern", + "capturedAtTurn": 8 + } + ], + "edges": [ + { + "fromItemIndex": 10, + "toItemIndex": 1, + "relation": "constrains" + }, + { + "fromItemIndex": 8, + "toItemIndex": 2, + "relation": "derived_from" + }, + { + "fromItemIndex": 9, + "toItemIndex": 0, + "relation": "constrains" + }, + { + "fromItemIndex": 12, + "toItemIndex": 2, + "relation": "depends_on" + }, + { + "fromItemIndex": 13, + "toItemIndex": 10, + "relation": "depends_on" + }, + { + "fromItemIndex": 15, + "toItemIndex": 12, + "relation": "constrains" + }, + { + "fromItemIndex": 14, + "toItemIndex": 11, + "relation": "depends_on" + } + ] + }, + "requirements-ready": { + "turns": [ + { + "phase": "scope", + "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?", + "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.", + "impact": "high", + "answer": "Replace our spreadsheet — we need proper tracking from creation to completion", + "options": [ + { + "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker", + "is_recommended": true + }, + { + "content": "Introduce issue tracking as a new practice — the team currently has no formal process", + "is_recommended": false + }, + { + "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability" + }, + { + "phase": "scope", + "question": "Who are the primary users of this system, and how large is the team?", + "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.", + "impact": "high", + "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility", + "options": [ + { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true }, + { + "content": "A medium team (10-30 people) with multiple squads and project leads", + "is_recommended": false + }, + { + "content": "A cross-functional group including developers, QA, product managers, and stakeholders", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards" + }, + { + "phase": "scope", + "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.", + "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.", + "impact": "high", + "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit", + "options": [ + { + "content": "Tickets (work items) with status, assignee, priority, and description", + "is_recommended": true + }, + { + "content": "Tickets plus epics or parent groupings for organizing related work", + "is_recommended": false + }, + { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates" + }, + { + "phase": "scope", + "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.", + "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.", + "impact": "high", + "answer": "Yes, we need full audit history for compliance — every status change must be traceable", + "options": [ + { + "content": "Full audit trail required — every status change must record who, what, and when", + "is_recommended": true + }, + { + "content": "Basic change history is nice to have but not a compliance requirement", + "is_recommended": false + }, + { + "content": "No audit requirements — simple current-state tracking is sufficient", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "scope", + "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.", + "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.", + "impact": "medium", + "answer": "Keep it simpler than Jira, add role-based permissions with three roles", + "options": [ + { + "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1", + "is_recommended": true + }, + { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false }, + { + "content": "Build a minimal viable tracker and expand based on user feedback", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We need role-based permissions though — admin, developer, and viewer roles" + }, + { + "phase": "scope", + "question": "", + "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.", + "isProposal": true + }, + { + "phase": "scope", + "question": "", + "answer": "Confirm scope closure", + "isConfirmation": true + }, + { + "phase": "design", + "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?", + "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.", + "impact": "high", + "answer": "Kanban board with swimlanes per assignee — that's how we think about work", + "options": [ + { + "content": "Kanban board with columns per status and optional swimlanes per assignee", + "is_recommended": true + }, + { + "content": "Sortable table view with inline editing and bulk actions", + "is_recommended": false + }, + { + "content": "Combined view with a board as default and table as an alternative", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "design", + "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.", + "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.", + "impact": "high", + "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket", + "options": [ + { + "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline", + "is_recommended": true + }, + { + "content": "Separate audit table with structured event records for each field change", + "is_recommended": false + }, + { + "content": "Event-sourced model where the ticket state is derived from an append-only event log", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket" + }, + { + "phase": "design", + "question": "", + "answer": "Design commitments are captured: Kanban board as primary view, comment-based activity log for audit, and API-layer role checks with three roles.", + "isProposal": true + }, + { + "phase": "design", + "question": "", + "answer": "Confirm design closure", + "isConfirmation": true + }, + { + "phase": "requirements", + "question": "Based on the scope and design commitments, I've synthesized the following requirement. Does this accurately capture the ticket creation workflow you need?", + "why": "Validating synthesized requirements against user intent ensures completeness and catches assumptions before they become implementation bugs.", + "impact": "high", + "answer": "Yes, that captures it — approve this requirement", + "options": [ + { + "content": "Approve — this requirement accurately captures the needed functionality", + "is_recommended": true + }, + { + "content": "Needs revision — the requirement is close but missing important details", + "is_recommended": false + }, + { "content": "Reject — this requirement doesn't belong in v1 scope", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers the audit trail implementation. Given your compliance needs and the comment-based design decision, does this requirement capture the right granularity?", + "why": "Audit requirements need precise specification — vague audit requirements lead to either over-engineering or compliance gaps.", + "impact": "high", + "answer": "Approve — status change audit logging is essential", + "options": [ + { + "content": "Approve — this captures the audit trail requirement at the right level of detail", + "is_recommended": true + }, + { + "content": "Needs more detail — specify exactly which fields trigger audit entries", + "is_recommended": false + }, + { "content": "Too granular — simplify to just tracking status changes", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers the permission model. Given the three-role design decision, does this capture the visibility rules correctly?", + "why": "Permission requirements are security-critical — ambiguous permission specs are a common source of authorization vulnerabilities.", + "impact": "high", + "answer": "Approve — the three-tier visibility model is correct", + "options": [ + { + "content": "Approve — the role-based visibility rules are correctly specified", + "is_recommended": true + }, + { + "content": "Needs adjustment — developers should see all tickets, not just assigned ones", + "is_recommended": false + }, + { "content": "Reject — we need a more granular permission model", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers ticket list filtering. Is this the right set of filter dimensions for v1?", + "why": "Filter capabilities directly affect usability — too few filters make the tool frustrating, too many add UI complexity.", + "impact": "medium", + "answer": "Approve — those four filter dimensions cover our daily needs", + "options": [ + { + "content": "Approve — status, assignee, priority, and date filters are sufficient for v1", + "is_recommended": true + }, + { + "content": "Add text search across title and description as a fifth filter", + "is_recommended": false + }, + { "content": "Remove date filtering — we rarely need it", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers CSV export for reporting. Given your constraint to keep things simpler than Jira, should CSV export be included in v1?", + "why": "Export features are often requested but rarely critical for v1 — deferring non-essential features keeps the initial scope tight.", + "impact": "low", + "answer": "Reject this one — CSV export can wait for v2", + "options": [ + { + "content": "Approve — CSV export is needed from day one for reporting workflows", + "is_recommended": false + }, + { + "content": "Reject — defer CSV export to v2; the team can use the UI for now", + "is_recommended": true + }, + { + "content": "Simplify — just support copy-paste of filtered views instead of full CSV export", + "is_recommended": false + } + ], + "selectedOptionPositions": [1], + "freeText": "We can manage without export for now — the spreadsheet transition doesn't require it" + }, + { + "phase": "requirements", + "question": "", + "answer": "Requirements are reviewed: 4 approved (ticket CRUD, audit logging, role-based visibility, filtering) and 1 rejected (CSV export deferred to v2). The requirement set has explicit review coverage.", + "isProposal": true + }, + { + "phase": "requirements", + "question": "", + "answer": "Confirm requirements closure", + "isConfirmation": true + } + ], + "knowledgeItems": [ + { + "kind": "goal", + "content": "Track work items from creation to completion with clear ownership and status visibility", + "capturedAtTurn": 0 + }, + { + "kind": "goal", + "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements", + "capturedAtTurn": 3 + }, + { + "kind": "goal", + "content": "Enable team-wide visibility into work distribution and project progress", + "capturedAtTurn": 1 + }, + { + "kind": "term", + "content": "ticket — A trackable unit of work with status, assignee, priority, and description", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "assignee — Team member responsible for completing a ticket", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "comment — Timestamped note attached to a ticket for discussion and status updates", + "capturedAtTurn": 2 + }, + { + "kind": "context", + "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility", + "capturedAtTurn": 1 + }, + { + "kind": "context", + "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability", + "capturedAtTurn": 0 + }, + { + "kind": "constraint", + "content": "Must be simpler than Jira — no custom workflows or custom fields in v1", + "capturedAtTurn": 4 + }, + { + "kind": "constraint", + "content": "Audit history required for compliance — every status change must be traceable", + "capturedAtTurn": 3 + }, + { + "kind": "constraint", + "content": "Role-based permissions with three roles: admin, developer, viewer", + "capturedAtTurn": 4 + }, + { + "kind": "decision", + "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee", + "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering", + "capturedAtTurn": 7 + }, + { + "kind": "decision", + "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline", + "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth", + "capturedAtTurn": 8 + }, + { + "kind": "decision", + "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)", + "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed", + "capturedAtTurn": 7 + }, + { + "kind": "assumption", + "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern", + "capturedAtTurn": 8 + }, + { + "kind": "requirement", + "content": "Create, edit, and close tickets with required fields: title, description, priority, and assignee", + "capturedAtTurn": 11, + "reviewAction": "reviewed", + "reviewedAtTurn": 11 + }, + { + "kind": "requirement", + "content": "Status change creates a timestamped audit log entry recording the actor identity, previous status, and new status", + "capturedAtTurn": 12, + "reviewAction": "reviewed", + "reviewedAtTurn": 12 + }, + { + "kind": "requirement", + "content": "Role-based visibility: admins see all tickets and settings, developers see assigned and unassigned tickets, viewers have read-only access", + "capturedAtTurn": 13, + "reviewAction": "reviewed", + "reviewedAtTurn": 13 + }, + { + "kind": "requirement", + "content": "Filter and sort ticket list by status, assignee, priority, and creation date", + "capturedAtTurn": 14, + "reviewAction": "reviewed", + "reviewedAtTurn": 14 + }, + { + "kind": "requirement", + "content": "Export ticket data as CSV for reporting", + "capturedAtTurn": 15, + "reviewAction": "rejected", + "reviewedAtTurn": 15 + } + ], + "edges": [ + { + "fromItemIndex": 10, + "toItemIndex": 1, + "relation": "constrains" + }, + { + "fromItemIndex": 8, + "toItemIndex": 2, + "relation": "derived_from" + }, + { + "fromItemIndex": 9, + "toItemIndex": 0, + "relation": "constrains" + }, + { + "fromItemIndex": 12, + "toItemIndex": 2, + "relation": "depends_on" + }, + { + "fromItemIndex": 13, + "toItemIndex": 10, + "relation": "depends_on" + }, + { + "fromItemIndex": 15, + "toItemIndex": 12, + "relation": "constrains" + }, + { + "fromItemIndex": 14, + "toItemIndex": 11, + "relation": "depends_on" + }, + { + "fromItemIndex": 18, + "toItemIndex": 0, + "relation": "refines" + }, + { + "fromItemIndex": 19, + "toItemIndex": 10, + "relation": "verifies" + }, + { + "fromItemIndex": 20, + "toItemIndex": 11, + "relation": "derived_from" + } + ] + }, + "criteria-ready": { + "turns": [ + { + "phase": "scope", + "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?", + "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.", + "impact": "high", + "answer": "Replace our spreadsheet — we need proper tracking from creation to completion", + "options": [ + { + "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker", + "is_recommended": true + }, + { + "content": "Introduce issue tracking as a new practice — the team currently has no formal process", + "is_recommended": false + }, + { + "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability" + }, + { + "phase": "scope", + "question": "Who are the primary users of this system, and how large is the team?", + "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.", + "impact": "high", + "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility", + "options": [ + { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true }, + { + "content": "A medium team (10-30 people) with multiple squads and project leads", + "is_recommended": false + }, + { + "content": "A cross-functional group including developers, QA, product managers, and stakeholders", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards" + }, + { + "phase": "scope", + "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.", + "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.", + "impact": "high", + "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit", + "options": [ + { + "content": "Tickets (work items) with status, assignee, priority, and description", + "is_recommended": true + }, + { + "content": "Tickets plus epics or parent groupings for organizing related work", + "is_recommended": false + }, + { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates" + }, + { + "phase": "scope", + "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.", + "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.", + "impact": "high", + "answer": "Yes, we need full audit history for compliance — every status change must be traceable", + "options": [ + { + "content": "Full audit trail required — every status change must record who, what, and when", + "is_recommended": true + }, + { + "content": "Basic change history is nice to have but not a compliance requirement", + "is_recommended": false + }, + { + "content": "No audit requirements — simple current-state tracking is sufficient", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "scope", + "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.", + "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.", + "impact": "medium", + "answer": "Keep it simpler than Jira, add role-based permissions with three roles", + "options": [ + { + "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1", + "is_recommended": true + }, + { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false }, + { + "content": "Build a minimal viable tracker and expand based on user feedback", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We need role-based permissions though — admin, developer, and viewer roles" + }, + { + "phase": "scope", + "question": "", + "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.", + "isProposal": true + }, + { + "phase": "scope", + "question": "", + "answer": "Confirm scope closure", + "isConfirmation": true + }, + { + "phase": "design", + "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?", + "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.", + "impact": "high", + "answer": "Kanban board with swimlanes per assignee — that's how we think about work", + "options": [ + { + "content": "Kanban board with columns per status and optional swimlanes per assignee", + "is_recommended": true + }, + { + "content": "Sortable table view with inline editing and bulk actions", + "is_recommended": false + }, + { + "content": "Combined view with a board as default and table as an alternative", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "design", + "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.", + "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.", + "impact": "high", + "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket", + "options": [ + { + "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline", + "is_recommended": true + }, + { + "content": "Separate audit table with structured event records for each field change", + "is_recommended": false + }, + { + "content": "Event-sourced model where the ticket state is derived from an append-only event log", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket" + }, + { + "phase": "design", + "question": "", + "answer": "Design commitments are captured: Kanban board as primary view, comment-based activity log for audit, and API-layer role checks with three roles.", + "isProposal": true + }, + { + "phase": "design", + "question": "", + "answer": "Confirm design closure", + "isConfirmation": true + }, + { + "phase": "requirements", + "question": "Based on the scope and design commitments, I've synthesized the following requirement. Does this accurately capture the ticket creation workflow you need?", + "why": "Validating synthesized requirements against user intent ensures completeness and catches assumptions before they become implementation bugs.", + "impact": "high", + "answer": "Yes, that captures it — approve this requirement", + "options": [ + { + "content": "Approve — this requirement accurately captures the needed functionality", + "is_recommended": true + }, + { + "content": "Needs revision — the requirement is close but missing important details", + "is_recommended": false + }, + { "content": "Reject — this requirement doesn't belong in v1 scope", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers the audit trail implementation. Given your compliance needs and the comment-based design decision, does this requirement capture the right granularity?", + "why": "Audit requirements need precise specification — vague audit requirements lead to either over-engineering or compliance gaps.", + "impact": "high", + "answer": "Approve — status change audit logging is essential", + "options": [ + { + "content": "Approve — this captures the audit trail requirement at the right level of detail", + "is_recommended": true + }, + { + "content": "Needs more detail — specify exactly which fields trigger audit entries", + "is_recommended": false + }, + { "content": "Too granular — simplify to just tracking status changes", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers the permission model. Given the three-role design decision, does this capture the visibility rules correctly?", + "why": "Permission requirements are security-critical — ambiguous permission specs are a common source of authorization vulnerabilities.", + "impact": "high", + "answer": "Approve — the three-tier visibility model is correct", + "options": [ + { + "content": "Approve — the role-based visibility rules are correctly specified", + "is_recommended": true + }, + { + "content": "Needs adjustment — developers should see all tickets, not just assigned ones", + "is_recommended": false + }, + { "content": "Reject — we need a more granular permission model", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers ticket list filtering. Is this the right set of filter dimensions for v1?", + "why": "Filter capabilities directly affect usability — too few filters make the tool frustrating, too many add UI complexity.", + "impact": "medium", + "answer": "Approve — those four filter dimensions cover our daily needs", + "options": [ + { + "content": "Approve — status, assignee, priority, and date filters are sufficient for v1", + "is_recommended": true + }, + { + "content": "Add text search across title and description as a fifth filter", + "is_recommended": false + }, + { "content": "Remove date filtering — we rarely need it", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers CSV export for reporting. Given your constraint to keep things simpler than Jira, should CSV export be included in v1?", + "why": "Export features are often requested but rarely critical for v1 — deferring non-essential features keeps the initial scope tight.", + "impact": "low", + "answer": "Reject this one — CSV export can wait for v2", + "options": [ + { + "content": "Approve — CSV export is needed from day one for reporting workflows", + "is_recommended": false + }, + { + "content": "Reject — defer CSV export to v2; the team can use the UI for now", + "is_recommended": true + }, + { + "content": "Simplify — just support copy-paste of filtered views instead of full CSV export", + "is_recommended": false + } + ], + "selectedOptionPositions": [1], + "freeText": "We can manage without export for now — the spreadsheet transition doesn't require it" + }, + { + "phase": "requirements", + "question": "", + "answer": "Requirements are reviewed: 4 approved (ticket CRUD, audit logging, role-based visibility, filtering) and 1 rejected (CSV export deferred to v2). The requirement set has explicit review coverage.", + "isProposal": true + }, + { + "phase": "requirements", + "question": "", + "answer": "Confirm requirements closure", + "isConfirmation": true + }, + { + "phase": "criteria", + "question": "This criterion verifies the audit trail requirement. Does it capture the right level of verification precision for the status change audit log?", + "why": "Audit criteria must be objectively testable — vague criteria lead to ambiguous acceptance testing and compliance gaps.", + "impact": "high", + "answer": "Approve — actor identity and ISO 8601 timestamp are the right verification points", + "options": [ + { "content": "Approve — this criterion is precise and testable", "is_recommended": true }, + { + "content": "Needs refinement — add verification of the previous and new status values", + "is_recommended": false + }, + { "content": "Too strict — ISO 8601 is overkill for an internal tool", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "This criterion verifies the permission model. Does the negative-case framing capture the security boundary correctly?", + "why": "Security criteria work best as negative constraints — verifying what cannot happen is often more important than what can.", + "impact": "high", + "answer": "Approve — the negative-case framing is exactly right for security boundaries", + "options": [ + { + "content": "Approve — non-admin deletion and audit modification are the right security boundaries to verify", + "is_recommended": true + }, + { + "content": "Expand — also verify that viewers cannot create or edit tickets", + "is_recommended": false + }, + { + "content": "Simplify — just verify that role checks exist, not specific denied operations", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "This criterion covers ticket list performance. Is 2 seconds with 500 tickets the right performance bar for v1?", + "why": "Performance criteria need concrete thresholds — without them, 'fast enough' is subjective and untestable.", + "impact": "medium", + "answer": "Approve — 2 seconds for 500 tickets is a reasonable v1 bar", + "options": [ + { + "content": "Approve — 2 seconds for 500 tickets with any filter is a reasonable v1 target", + "is_recommended": true + }, + { + "content": "Tighten — should be under 1 second for the team size we're targeting", + "is_recommended": false + }, + { + "content": "Relax — 5 seconds is acceptable for v1 since the team is small", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "This criterion covers CSV export verification. Since the CSV export requirement was rejected, should this criterion also be rejected?", + "why": "Criteria that verify rejected requirements should typically be rejected to maintain consistency between the requirement and criteria sets.", + "impact": "low", + "answer": "Reject — this is coupled to the rejected CSV export requirement", + "options": [ + { + "content": "Reject — this criterion is moot since the underlying requirement was deferred to v2", + "is_recommended": true + }, + { + "content": "Keep as deferred — mark it for v2 alongside the requirement", + "is_recommended": false + }, + { + "content": "Approve anyway — we might add CSV export during implementation", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + } + ], + "knowledgeItems": [ + { + "kind": "goal", + "content": "Track work items from creation to completion with clear ownership and status visibility", + "capturedAtTurn": 0 + }, + { + "kind": "goal", + "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements", + "capturedAtTurn": 3 + }, + { + "kind": "goal", + "content": "Enable team-wide visibility into work distribution and project progress", + "capturedAtTurn": 1 + }, + { + "kind": "term", + "content": "ticket — A trackable unit of work with status, assignee, priority, and description", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "assignee — Team member responsible for completing a ticket", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "comment — Timestamped note attached to a ticket for discussion and status updates", + "capturedAtTurn": 2 + }, + { + "kind": "context", + "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility", + "capturedAtTurn": 1 + }, + { + "kind": "context", + "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability", + "capturedAtTurn": 0 + }, + { + "kind": "constraint", + "content": "Must be simpler than Jira — no custom workflows or custom fields in v1", + "capturedAtTurn": 4 + }, + { + "kind": "constraint", + "content": "Audit history required for compliance — every status change must be traceable", + "capturedAtTurn": 3 + }, + { + "kind": "constraint", + "content": "Role-based permissions with three roles: admin, developer, viewer", + "capturedAtTurn": 4 + }, + { + "kind": "decision", + "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee", + "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering", + "capturedAtTurn": 7 + }, + { + "kind": "decision", + "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline", + "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth", + "capturedAtTurn": 8 + }, + { + "kind": "decision", + "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)", + "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed", + "capturedAtTurn": 7 + }, + { + "kind": "assumption", + "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern", + "capturedAtTurn": 8 + }, + { + "kind": "requirement", + "content": "Create, edit, and close tickets with required fields: title, description, priority, and assignee", + "capturedAtTurn": 11, + "reviewAction": "reviewed", + "reviewedAtTurn": 11 + }, + { + "kind": "requirement", + "content": "Status change creates a timestamped audit log entry recording the actor identity, previous status, and new status", + "capturedAtTurn": 12, + "reviewAction": "reviewed", + "reviewedAtTurn": 12 + }, + { + "kind": "requirement", + "content": "Role-based visibility: admins see all tickets and settings, developers see assigned and unassigned tickets, viewers have read-only access", + "capturedAtTurn": 13, + "reviewAction": "reviewed", + "reviewedAtTurn": 13 + }, + { + "kind": "requirement", + "content": "Filter and sort ticket list by status, assignee, priority, and creation date", + "capturedAtTurn": 14, + "reviewAction": "reviewed", + "reviewedAtTurn": 14 + }, + { + "kind": "requirement", + "content": "Export ticket data as CSV for reporting", + "capturedAtTurn": 15, + "reviewAction": "rejected", + "reviewedAtTurn": 15 + }, + { + "kind": "criterion", + "content": "Every status change records the actor identity and ISO 8601 timestamp in the audit log", + "capturedAtTurn": 19, + "reviewAction": "reviewed", + "reviewedAtTurn": 19 + }, + { + "kind": "criterion", + "content": "Non-admin users cannot delete tickets or modify audit entries", + "capturedAtTurn": 20, + "reviewAction": "reviewed", + "reviewedAtTurn": 20 + }, + { + "kind": "criterion", + "content": "Ticket list loads within 2 seconds for up to 500 tickets with any filter combination", + "capturedAtTurn": 21, + "reviewAction": "reviewed", + "reviewedAtTurn": 21 + }, + { + "kind": "criterion", + "content": "CSV export includes all visible fields and respects role-based visibility filters", + "capturedAtTurn": 22, + "reviewAction": "rejected", + "reviewedAtTurn": 22 + } + ], + "edges": [ + { + "fromItemIndex": 10, + "toItemIndex": 1, + "relation": "constrains" + }, + { + "fromItemIndex": 8, + "toItemIndex": 2, + "relation": "derived_from" + }, + { + "fromItemIndex": 9, + "toItemIndex": 0, + "relation": "constrains" + }, + { + "fromItemIndex": 12, + "toItemIndex": 2, + "relation": "depends_on" + }, + { + "fromItemIndex": 13, + "toItemIndex": 10, + "relation": "depends_on" + }, + { + "fromItemIndex": 15, + "toItemIndex": 12, + "relation": "constrains" + }, + { + "fromItemIndex": 14, + "toItemIndex": 11, + "relation": "depends_on" + }, + { + "fromItemIndex": 18, + "toItemIndex": 0, + "relation": "refines" + }, + { + "fromItemIndex": 19, + "toItemIndex": 10, + "relation": "verifies" + }, + { + "fromItemIndex": 20, + "toItemIndex": 11, + "relation": "derived_from" + }, + { + "fromItemIndex": 23, + "toItemIndex": 19, + "relation": "verifies" + }, + { + "fromItemIndex": 24, + "toItemIndex": 20, + "relation": "verifies" + }, + { + "fromItemIndex": 25, + "toItemIndex": 21, + "relation": "constrains" + }, + { + "fromItemIndex": 26, + "toItemIndex": 22, + "relation": "verifies" + } + ] + }, + "all-phases-closed": { + "turns": [ + { + "phase": "scope", + "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?", + "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.", + "impact": "high", + "answer": "Replace our spreadsheet — we need proper tracking from creation to completion", + "options": [ + { + "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker", + "is_recommended": true + }, + { + "content": "Introduce issue tracking as a new practice — the team currently has no formal process", + "is_recommended": false + }, + { + "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability" + }, + { + "phase": "scope", + "question": "Who are the primary users of this system, and how large is the team?", + "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.", + "impact": "high", + "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility", + "options": [ + { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true }, + { + "content": "A medium team (10-30 people) with multiple squads and project leads", + "is_recommended": false + }, + { + "content": "A cross-functional group including developers, QA, product managers, and stakeholders", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards" + }, + { + "phase": "scope", + "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.", + "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.", + "impact": "high", + "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit", + "options": [ + { + "content": "Tickets (work items) with status, assignee, priority, and description", + "is_recommended": true + }, + { + "content": "Tickets plus epics or parent groupings for organizing related work", + "is_recommended": false + }, + { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates" + }, + { + "phase": "scope", + "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.", + "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.", + "impact": "high", + "answer": "Yes, we need full audit history for compliance — every status change must be traceable", + "options": [ + { + "content": "Full audit trail required — every status change must record who, what, and when", + "is_recommended": true + }, + { + "content": "Basic change history is nice to have but not a compliance requirement", + "is_recommended": false + }, + { + "content": "No audit requirements — simple current-state tracking is sufficient", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "scope", + "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.", + "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.", + "impact": "medium", + "answer": "Keep it simpler than Jira, add role-based permissions with three roles", + "options": [ + { + "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1", + "is_recommended": true + }, + { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false }, + { + "content": "Build a minimal viable tracker and expand based on user feedback", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "We need role-based permissions though — admin, developer, and viewer roles" + }, + { + "phase": "scope", + "question": "", + "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.", + "isProposal": true + }, + { + "phase": "scope", + "question": "", + "answer": "Confirm scope closure", + "isConfirmation": true + }, + { + "phase": "design", + "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?", + "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.", + "impact": "high", + "answer": "Kanban board with swimlanes per assignee — that's how we think about work", + "options": [ + { + "content": "Kanban board with columns per status and optional swimlanes per assignee", + "is_recommended": true + }, + { + "content": "Sortable table view with inline editing and bulk actions", + "is_recommended": false + }, + { + "content": "Combined view with a board as default and table as an alternative", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "design", + "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.", + "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.", + "impact": "high", + "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket", + "options": [ + { + "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline", + "is_recommended": true + }, + { + "content": "Separate audit table with structured event records for each field change", + "is_recommended": false + }, + { + "content": "Event-sourced model where the ticket state is derived from an append-only event log", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket" + }, + { + "phase": "design", + "question": "", + "answer": "Design commitments are captured: Kanban board as primary view, comment-based activity log for audit, and API-layer role checks with three roles.", + "isProposal": true + }, + { + "phase": "design", + "question": "", + "answer": "Confirm design closure", + "isConfirmation": true + }, + { + "phase": "requirements", + "question": "Based on the scope and design commitments, I've synthesized the following requirement. Does this accurately capture the ticket creation workflow you need?", + "why": "Validating synthesized requirements against user intent ensures completeness and catches assumptions before they become implementation bugs.", + "impact": "high", + "answer": "Yes, that captures it — approve this requirement", + "options": [ + { + "content": "Approve — this requirement accurately captures the needed functionality", + "is_recommended": true + }, + { + "content": "Needs revision — the requirement is close but missing important details", + "is_recommended": false + }, + { "content": "Reject — this requirement doesn't belong in v1 scope", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers the audit trail implementation. Given your compliance needs and the comment-based design decision, does this requirement capture the right granularity?", + "why": "Audit requirements need precise specification — vague audit requirements lead to either over-engineering or compliance gaps.", + "impact": "high", + "answer": "Approve — status change audit logging is essential", + "options": [ + { + "content": "Approve — this captures the audit trail requirement at the right level of detail", + "is_recommended": true + }, + { + "content": "Needs more detail — specify exactly which fields trigger audit entries", + "is_recommended": false + }, + { "content": "Too granular — simplify to just tracking status changes", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers the permission model. Given the three-role design decision, does this capture the visibility rules correctly?", + "why": "Permission requirements are security-critical — ambiguous permission specs are a common source of authorization vulnerabilities.", + "impact": "high", + "answer": "Approve — the three-tier visibility model is correct", + "options": [ + { + "content": "Approve — the role-based visibility rules are correctly specified", + "is_recommended": true + }, + { + "content": "Needs adjustment — developers should see all tickets, not just assigned ones", + "is_recommended": false + }, + { "content": "Reject — we need a more granular permission model", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers ticket list filtering. Is this the right set of filter dimensions for v1?", + "why": "Filter capabilities directly affect usability — too few filters make the tool frustrating, too many add UI complexity.", + "impact": "medium", + "answer": "Approve — those four filter dimensions cover our daily needs", + "options": [ + { + "content": "Approve — status, assignee, priority, and date filters are sufficient for v1", + "is_recommended": true + }, + { + "content": "Add text search across title and description as a fifth filter", + "is_recommended": false + }, + { "content": "Remove date filtering — we rarely need it", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "requirements", + "question": "This requirement covers CSV export for reporting. Given your constraint to keep things simpler than Jira, should CSV export be included in v1?", + "why": "Export features are often requested but rarely critical for v1 — deferring non-essential features keeps the initial scope tight.", + "impact": "low", + "answer": "Reject this one — CSV export can wait for v2", + "options": [ + { + "content": "Approve — CSV export is needed from day one for reporting workflows", + "is_recommended": false + }, + { + "content": "Reject — defer CSV export to v2; the team can use the UI for now", + "is_recommended": true + }, + { + "content": "Simplify — just support copy-paste of filtered views instead of full CSV export", + "is_recommended": false + } + ], + "selectedOptionPositions": [1], + "freeText": "We can manage without export for now — the spreadsheet transition doesn't require it" + }, + { + "phase": "requirements", + "question": "", + "answer": "Requirements are reviewed: 4 approved (ticket CRUD, audit logging, role-based visibility, filtering) and 1 rejected (CSV export deferred to v2). The requirement set has explicit review coverage.", + "isProposal": true + }, + { + "phase": "requirements", + "question": "", + "answer": "Confirm requirements closure", + "isConfirmation": true + }, + { + "phase": "criteria", + "question": "This criterion verifies the audit trail requirement. Does it capture the right level of verification precision for the status change audit log?", + "why": "Audit criteria must be objectively testable — vague criteria lead to ambiguous acceptance testing and compliance gaps.", + "impact": "high", + "answer": "Approve — actor identity and ISO 8601 timestamp are the right verification points", + "options": [ + { "content": "Approve — this criterion is precise and testable", "is_recommended": true }, + { + "content": "Needs refinement — add verification of the previous and new status values", + "is_recommended": false + }, + { "content": "Too strict — ISO 8601 is overkill for an internal tool", "is_recommended": false } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "This criterion verifies the permission model. Does the negative-case framing capture the security boundary correctly?", + "why": "Security criteria work best as negative constraints — verifying what cannot happen is often more important than what can.", + "impact": "high", + "answer": "Approve — the negative-case framing is exactly right for security boundaries", + "options": [ + { + "content": "Approve — non-admin deletion and audit modification are the right security boundaries to verify", + "is_recommended": true + }, + { + "content": "Expand — also verify that viewers cannot create or edit tickets", + "is_recommended": false + }, + { + "content": "Simplify — just verify that role checks exist, not specific denied operations", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "This criterion covers ticket list performance. Is 2 seconds with 500 tickets the right performance bar for v1?", + "why": "Performance criteria need concrete thresholds — without them, 'fast enough' is subjective and untestable.", + "impact": "medium", + "answer": "Approve — 2 seconds for 500 tickets is a reasonable v1 bar", + "options": [ + { + "content": "Approve — 2 seconds for 500 tickets with any filter is a reasonable v1 target", + "is_recommended": true + }, + { + "content": "Tighten — should be under 1 second for the team size we're targeting", + "is_recommended": false + }, + { + "content": "Relax — 5 seconds is acceptable for v1 since the team is small", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "This criterion covers CSV export verification. Since the CSV export requirement was rejected, should this criterion also be rejected?", + "why": "Criteria that verify rejected requirements should typically be rejected to maintain consistency between the requirement and criteria sets.", + "impact": "low", + "answer": "Reject — this is coupled to the rejected CSV export requirement", + "options": [ + { + "content": "Reject — this criterion is moot since the underlying requirement was deferred to v2", + "is_recommended": true + }, + { + "content": "Keep as deferred — mark it for v2 alongside the requirement", + "is_recommended": false + }, + { + "content": "Approve anyway — we might add CSV export during implementation", + "is_recommended": false + } + ], + "selectedOptionPositions": [0], + "freeText": null + }, + { + "phase": "criteria", + "question": "", + "answer": "Criteria review is complete: 3 approved (audit log verification, permission boundaries, performance threshold) and 1 rejected (CSV export verification, coupled to rejected requirement).", + "isProposal": true + }, + { + "phase": "criteria", + "question": "", + "answer": "Confirm criteria closure", + "isConfirmation": true + } + ], + "knowledgeItems": [ + { + "kind": "goal", + "content": "Track work items from creation to completion with clear ownership and status visibility", + "capturedAtTurn": 0 + }, + { + "kind": "goal", + "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements", + "capturedAtTurn": 3 + }, + { + "kind": "goal", + "content": "Enable team-wide visibility into work distribution and project progress", + "capturedAtTurn": 1 + }, + { + "kind": "term", + "content": "ticket — A trackable unit of work with status, assignee, priority, and description", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "assignee — Team member responsible for completing a ticket", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)", + "capturedAtTurn": 2 + }, + { + "kind": "term", + "content": "comment — Timestamped note attached to a ticket for discussion and status updates", + "capturedAtTurn": 2 + }, + { + "kind": "context", + "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility", + "capturedAtTurn": 1 + }, + { + "kind": "context", + "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability", + "capturedAtTurn": 0 + }, + { + "kind": "constraint", + "content": "Must be simpler than Jira — no custom workflows or custom fields in v1", + "capturedAtTurn": 4 + }, + { + "kind": "constraint", + "content": "Audit history required for compliance — every status change must be traceable", + "capturedAtTurn": 3 + }, + { + "kind": "constraint", + "content": "Role-based permissions with three roles: admin, developer, viewer", + "capturedAtTurn": 4 + }, + { + "kind": "decision", + "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee", + "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering", + "capturedAtTurn": 7 + }, + { + "kind": "decision", + "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline", + "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth", + "capturedAtTurn": 8 + }, + { + "kind": "decision", + "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)", + "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed", + "capturedAtTurn": 7 + }, + { + "kind": "assumption", + "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs", + "capturedAtTurn": 8 + }, + { + "kind": "assumption", + "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern", + "capturedAtTurn": 8 + }, + { + "kind": "requirement", + "content": "Create, edit, and close tickets with required fields: title, description, priority, and assignee", + "capturedAtTurn": 11, + "reviewAction": "reviewed", + "reviewedAtTurn": 11 + }, + { + "kind": "requirement", + "content": "Status change creates a timestamped audit log entry recording the actor identity, previous status, and new status", + "capturedAtTurn": 12, + "reviewAction": "reviewed", + "reviewedAtTurn": 12 + }, + { + "kind": "requirement", + "content": "Role-based visibility: admins see all tickets and settings, developers see assigned and unassigned tickets, viewers have read-only access", + "capturedAtTurn": 13, + "reviewAction": "reviewed", + "reviewedAtTurn": 13 + }, + { + "kind": "requirement", + "content": "Filter and sort ticket list by status, assignee, priority, and creation date", + "capturedAtTurn": 14, + "reviewAction": "reviewed", + "reviewedAtTurn": 14 + }, + { + "kind": "requirement", + "content": "Export ticket data as CSV for reporting", + "capturedAtTurn": 15, + "reviewAction": "rejected", + "reviewedAtTurn": 15 + }, + { + "kind": "criterion", + "content": "Every status change records the actor identity and ISO 8601 timestamp in the audit log", + "capturedAtTurn": 19, + "reviewAction": "reviewed", + "reviewedAtTurn": 19 + }, + { + "kind": "criterion", + "content": "Non-admin users cannot delete tickets or modify audit entries", + "capturedAtTurn": 20, + "reviewAction": "reviewed", + "reviewedAtTurn": 20 + }, + { + "kind": "criterion", + "content": "Ticket list loads within 2 seconds for up to 500 tickets with any filter combination", + "capturedAtTurn": 21, + "reviewAction": "reviewed", + "reviewedAtTurn": 21 + }, + { + "kind": "criterion", + "content": "CSV export includes all visible fields and respects role-based visibility filters", + "capturedAtTurn": 22, + "reviewAction": "rejected", + "reviewedAtTurn": 22 + } + ], + "edges": [ + { + "fromItemIndex": 10, + "toItemIndex": 1, + "relation": "constrains" + }, + { + "fromItemIndex": 8, + "toItemIndex": 2, + "relation": "derived_from" + }, + { + "fromItemIndex": 9, + "toItemIndex": 0, + "relation": "constrains" + }, + { + "fromItemIndex": 12, + "toItemIndex": 2, + "relation": "depends_on" + }, + { + "fromItemIndex": 13, + "toItemIndex": 10, + "relation": "depends_on" + }, + { + "fromItemIndex": 15, + "toItemIndex": 12, + "relation": "constrains" + }, + { + "fromItemIndex": 14, + "toItemIndex": 11, + "relation": "depends_on" + }, + { + "fromItemIndex": 18, + "toItemIndex": 0, + "relation": "refines" + }, + { + "fromItemIndex": 19, + "toItemIndex": 10, + "relation": "verifies" + }, + { + "fromItemIndex": 20, + "toItemIndex": 11, + "relation": "derived_from" + }, + { + "fromItemIndex": 23, + "toItemIndex": 19, + "relation": "verifies" + }, + { + "fromItemIndex": 24, + "toItemIndex": 20, + "relation": "verifies" + }, + { + "fromItemIndex": 25, + "toItemIndex": 21, + "relation": "constrains" + }, + { + "fromItemIndex": 26, + "toItemIndex": 22, + "relation": "verifies" + } + ] + } + } +} diff --git a/src/server/fixtures/scenarios.ts b/src/server/fixtures/scenarios.ts index d7953a82..9901dba4 100644 --- a/src/server/fixtures/scenarios.ts +++ b/src/server/fixtures/scenarios.ts @@ -3,12 +3,23 @@ import { confirmPhaseOutcome, createKnowledgeItem, createPhaseOutcome, + createConfirmedPhaseOutcome, createProject, createTurn, linkKnowledgeItemToTurn, type DB, } from '../db.js'; +function createConfirmationParts(text: string, data: object): string { + return JSON.stringify([ + { type: 'text', text }, + { + type: 'data-confirmation', + data, + }, + ]); +} + export function seedClosedScope(db: DB, projectId: number) { const scopeTurn = createTurn(db, projectId, { phase: 'scope', @@ -37,17 +48,11 @@ export function seedClosedScope(db: DB, projectId: number) { parent_turn_id: scopeProposalTurn.id, question: '', answer: 'Confirm scope closure', - user_parts: JSON.stringify([ - { type: 'text', text: 'Confirm scope closure' }, - { - type: 'data-confirmation', - data: { - kind: 'confirm-proposed-phase-closure', - proposalTurnId: scopeProposalTurn.id, - phase: 'scope', - }, - }, - ]), + user_parts: createConfirmationParts('Confirm scope closure', { + kind: 'confirm-proposed-phase-closure', + proposalTurnId: scopeProposalTurn.id, + phase: 'scope', + }), }); confirmPhaseOutcome(db, scopeOutcome.id, scopeConfirmationTurn.id); advanceHead(db, projectId, scopeConfirmationTurn.id); @@ -84,17 +89,11 @@ export function seedRequirementsReady(db: DB, projectId: number) { parent_turn_id: seededDesign.designTurn.id, question: '', answer: 'Confirm design closure', - user_parts: JSON.stringify([ - { type: 'text', text: 'Confirm design closure' }, - { - type: 'data-confirmation', - data: { - kind: 'confirm-proposed-phase-closure', - proposalTurnId: seededDesign.designTurn.id, - phase: 'design', - }, - }, - ]), + user_parts: createConfirmationParts('Confirm design closure', { + kind: 'confirm-proposed-phase-closure', + proposalTurnId: seededDesign.designTurn.id, + phase: 'design', + }), }); confirmPhaseOutcome(db, designOutcome.id, designConfirmationTurn.id); advanceHead(db, projectId, designConfirmationTurn.id); @@ -102,9 +101,7 @@ export function seedRequirementsReady(db: DB, projectId: number) { return { ...seededDesign, designConfirmationTurn }; } -export function seedCriteriaReady(db: DB, projectId: number) { - const seededRequirements = seedRequirementsReady(db, projectId); - +function seedClosedRequirementsReview(db: DB, projectId: number, parentTurnId: number) { const approvedRequirement = createKnowledgeItem( db, projectId, @@ -120,7 +117,7 @@ export function seedCriteriaReady(db: DB, projectId: number) { const reviewTurn = createTurn(db, projectId, { phase: 'requirements', - parent_turn_id: seededRequirements.designConfirmationTurn.id, + parent_turn_id: parentTurnId, question: 'Are these requirements all reviewed now?', answer: 'Yes — approve resume and reject PDF export', }); @@ -148,23 +145,16 @@ export function seedCriteriaReady(db: DB, projectId: number) { parent_turn_id: requirementsProposalTurn.id, question: '', answer: 'Confirm requirements closure', - user_parts: JSON.stringify([ - { type: 'text', text: 'Confirm requirements closure' }, - { - type: 'data-confirmation', - data: { - kind: 'confirm-proposed-phase-closure', - proposalTurnId: requirementsProposalTurn.id, - phase: 'requirements', - }, - }, - ]), + user_parts: createConfirmationParts('Confirm requirements closure', { + kind: 'confirm-proposed-phase-closure', + proposalTurnId: requirementsProposalTurn.id, + phase: 'requirements', + }), }); confirmPhaseOutcome(db, requirementsOutcome.id, requirementsConfirmationTurn.id); advanceHead(db, projectId, requirementsConfirmationTurn.id); return { - ...seededRequirements, approvedRequirement, rejectedRequirement, reviewTurn, @@ -173,13 +163,22 @@ export function seedCriteriaReady(db: DB, projectId: number) { }; } -export function seedAllPhasesClosed(db: DB, projectId: number) { - const seededCriteria = seedCriteriaReady(db, projectId); +export function seedCriteriaReady(db: DB, projectId: number) { + const seededRequirements = seedRequirementsReady(db, projectId); + const reviewedRequirements = seedClosedRequirementsReview( + db, + projectId, + seededRequirements.designConfirmationTurn.id, + ); + return { ...seededRequirements, ...reviewedRequirements }; +} + +function seedClosedCriteriaReview(db: DB, projectId: number, parentTurnId: number) { const criterion = createKnowledgeItem(db, projectId, 'criterion', 'Verify SQLite resume'); const criterionReviewTurn = createTurn(db, projectId, { phase: 'criteria', - parent_turn_id: seededCriteria.requirementsConfirmationTurn.id, + parent_turn_id: parentTurnId, question: 'Are these criteria reviewed?', answer: 'Yes — approve the criterion', }); @@ -206,27 +205,149 @@ export function seedAllPhasesClosed(db: DB, projectId: number) { parent_turn_id: criteriaProposalTurn.id, question: '', answer: 'Confirm criteria closure', - user_parts: JSON.stringify([ - { type: 'text', text: 'Confirm criteria closure' }, - { - type: 'data-confirmation', - data: { - kind: 'confirm-proposed-phase-closure', - proposalTurnId: criteriaProposalTurn.id, - phase: 'criteria', - }, - }, - ]), + user_parts: createConfirmationParts('Confirm criteria closure', { + kind: 'confirm-proposed-phase-closure', + proposalTurnId: criteriaProposalTurn.id, + phase: 'criteria', + }), }); confirmPhaseOutcome(db, criteriaOutcome.id, criteriaConfirmationTurn.id); advanceHead(db, projectId, criteriaConfirmationTurn.id); + return { criterion, criterionReviewTurn, criteriaProposalTurn, criteriaConfirmationTurn }; +} + +export function seedAllPhasesClosed(db: DB, projectId: number) { + const seededCriteria = seedCriteriaReady(db, projectId); + const reviewedCriteria = seedClosedCriteriaReview( + db, + projectId, + seededCriteria.requirementsConfirmationTurn.id, + ); + + return { ...seededCriteria, ...reviewedCriteria }; +} + +export function seedAllPhasesClosedWithForcedDesign(db: DB, projectId: number) { + const seededScope = seedClosedScope(db, projectId); + + const designTurn = createTurn(db, projectId, { + phase: 'design', + parent_turn_id: seededScope.scopeConfirmationTurn.id, + question: 'Which tradeoff matters most?', + answer: 'Keep the repository seam small', + }); + advanceHead(db, projectId, designTurn.id); + + const designForceCloseTurn = createTurn(db, projectId, { + phase: 'design', + parent_turn_id: designTurn.id, + question: '', + answer: 'Force design closure', + user_parts: createConfirmationParts('Force design closure', { + kind: 'force-close-active-phase', + phase: 'design', + }), + }); + advanceHead(db, projectId, designForceCloseTurn.id); + + const designOutcome = createPhaseOutcome(db, { + projectId, + phase: 'design', + proposal_turn_id: designForceCloseTurn.id, + summary: 'Design closed by user without an interviewer recommendation.', + }); + confirmPhaseOutcome(db, designOutcome.id, designForceCloseTurn.id); + + const reviewedRequirements = seedClosedRequirementsReview(db, projectId, designForceCloseTurn.id); + const reviewedCriteria = seedClosedCriteriaReview( + db, + projectId, + reviewedRequirements.requirementsConfirmationTurn.id, + ); + return { - ...seededCriteria, - criterion, - criterionReviewTurn, - criteriaProposalTurn, - criteriaConfirmationTurn, + ...seededScope, + designTurn, + designForceCloseTurn, + ...reviewedRequirements, + ...reviewedCriteria, + }; +} + +export function seedAllPhasesClosedWithLowReadinessScope(db: DB, projectId: number) { + const designTurn = createTurn(db, projectId, { + phase: 'design', + question: 'Which tradeoff matters most?', + answer: 'Keep the repository seam small', + }); + advanceHead(db, projectId, designTurn.id); + + const scopeClosureTurn = createTurn(db, projectId, { + phase: 'design', + parent_turn_id: designTurn.id, + question: '', + answer: 'Confirm scope closure', + user_parts: createConfirmationParts('Confirm scope closure', { + kind: 'confirm-proposed-phase-closure', + proposalTurnId: designTurn.id, + phase: 'scope', + }), + }); + advanceHead(db, projectId, scopeClosureTurn.id); + + createConfirmedPhaseOutcome(db, { + projectId, + phase: 'scope', + proposal_turn_id: scopeClosureTurn.id, + confirmation_turn_id: scopeClosureTurn.id, + summary: + 'Scope was closed from a minimal downstream checkpoint to exercise low-readiness export caveats.', + }); + + const designProposalTurn = createTurn(db, projectId, { + phase: 'design', + parent_turn_id: scopeClosureTurn.id, + question: '', + answer: 'The main architectural commitments are captured well enough to review requirements.', + }); + advanceHead(db, projectId, designProposalTurn.id); + + const designConfirmationTurn = createTurn(db, projectId, { + phase: 'design', + parent_turn_id: designProposalTurn.id, + question: '', + answer: 'Confirm design closure', + user_parts: createConfirmationParts('Confirm design closure', { + kind: 'confirm-proposed-phase-closure', + proposalTurnId: designProposalTurn.id, + phase: 'design', + }), + }); + advanceHead(db, projectId, designConfirmationTurn.id); + + const designOutcome = createPhaseOutcome(db, { + projectId, + phase: 'design', + proposal_turn_id: designProposalTurn.id, + summary: 'The main architectural commitments are captured well enough to review requirements.', + }); + confirmPhaseOutcome(db, designOutcome.id, designConfirmationTurn.id); + + const reviewedRequirements = seedClosedRequirementsReview(db, projectId, designConfirmationTurn.id); + const reviewedCriteria = seedClosedCriteriaReview( + db, + projectId, + reviewedRequirements.requirementsConfirmationTurn.id, + ); + + return { + designTurn, + scopeClosureTurn, + designProposalTurn, + designConfirmationTurn, + ...reviewedRequirements, + ...reviewedCriteria, }; } @@ -258,6 +379,26 @@ export const scenarios: Record = { seedAllPhasesClosed(db, project.id); return project.id; }, + 'forced-close-all-phases-closed': (db, name = 'Forced-Close All Phases Closed') => { + const project = createProject(db, name); + seedAllPhasesClosedWithForcedDesign(db, project.id); + return project.id; + }, + 'low-readiness-all-phases-closed': (db, name = 'Low-Readiness All Phases Closed') => { + const project = createProject(db, name); + seedAllPhasesClosedWithLowReadinessScope(db, project.id); + return project.id; + }, }; -export const scenarioNames = Object.keys(scenarios); +// Manifest-based scenarios (additive — issue-tracker domain with rich parts + knowledge) +let manifestScenarios: Record = {}; +try { + const { loadManifestScenarios } = await import('./manifest.js'); + manifestScenarios = loadManifestScenarios('issue-tracker'); +} catch { + // Manifest files may not exist in all environments (e.g., CI without fixtures) +} + +export const allScenarios: Record = { ...scenarios, ...manifestScenarios }; +export const scenarioNames = Object.keys(allScenarios); diff --git a/src/server/fixtures/seed.ts b/src/server/fixtures/seed.ts index 4c9bed52..1a6d51e8 100644 --- a/src/server/fixtures/seed.ts +++ b/src/server/fixtures/seed.ts @@ -1,16 +1,16 @@ import { createDb } from '../db.js'; -import { scenarioNames, scenarios } from './scenarios.js'; +import { allScenarios, scenarioNames } from './scenarios.js'; const args = process.argv.slice(2); const scenarioName = args[0]; const dbPath = args[1] ?? './brunch.db'; -if (!scenarioName || !scenarios[scenarioName]) { +if (!scenarioName || !allScenarios[scenarioName]) { console.error(scenarioName ? `Unknown scenario: ${scenarioName}` : 'Usage: seed [db-path]'); console.error(`\nAvailable scenarios:\n${scenarioNames.map((n) => ` - ${n}`).join('\n')}`); process.exit(1); } const db = createDb(dbPath); -const projectId = scenarios[scenarioName](db); +const projectId = allScenarios[scenarioName](db); console.log(`Seeded "${scenarioName}" → project ${projectId} in ${dbPath}`); diff --git a/src/server/interview.test.ts b/src/server/interview.test.ts index 429e9b2e..8b27688a 100644 --- a/src/server/interview.test.ts +++ b/src/server/interview.test.ts @@ -44,6 +44,38 @@ describe('structuredQuestionSchema', () => { }), ).toThrow(); }); + + it('accepts the legacy review field and normalizes it to requirementReview', () => { + expect( + structuredQuestionSchema.parse({ + question: 'Should we approve this requirement?', + why: 'Requirement review coverage should continue to work during the prompt transition.', + impact: 'high', + options: [ + { content: 'Approve', is_recommended: true }, + { content: 'Reject', is_recommended: false }, + ], + review: { + kind: 'requirement-approval', + requirementId: 42, + approveOptionPosition: 0, + }, + }), + ).toEqual({ + question: 'Should we approve this requirement?', + why: 'Requirement review coverage should continue to work during the prompt transition.', + impact: 'high', + options: [ + { content: 'Approve', is_recommended: true }, + { content: 'Reject', is_recommended: false }, + ], + requirementReview: { + kind: 'requirement-approval', + requirementId: 42, + approveOptionPosition: 0, + }, + }); + }); }); describe('getSystemPrompt', () => { @@ -64,6 +96,7 @@ describe('getSystemPrompt', () => { expect(getSystemPrompt('requirements')).toContain('current requirement inventory'); expect(getSystemPrompt('requirements')).toContain('requirement-approval'); expect(getSystemPrompt('requirements')).toContain('requirement-rejection'); + expect(getSystemPrompt('requirements')).toContain('requirementReview'); expect(getSystemPrompt('requirements')).toContain('propose_phase_closure'); }); }); diff --git a/src/server/interview.ts b/src/server/interview.ts index 93f3fceb..808f0fc5 100644 --- a/src/server/interview.ts +++ b/src/server/interview.ts @@ -55,9 +55,9 @@ When the main architectural commitments are sufficiently captured for now, use t Your job is to walk the accumulated requirements, check for gaps, suggest additions, and confirm completeness. Ground each review turn in the current requirement inventory provided in context. Present requirements for the user to confirm, modify, or flag as missing. -When asking the user to approve one specific requirement, review one requirement at a time and include \`review: { kind: 'requirement-approval', requirementId, approveOptionPosition }\` in the ask_question input so the approval target is explicit. +When asking the user to approve one specific requirement, review one requirement at a time and include \`requirementReview: { kind: 'requirement-approval', requirementId, approveOptionPosition }\` in the ask_question input so the approval target is explicit. -When asking the user to reject one specific requirement, include \`review: { kind: 'requirement-rejection', requirementId, rejectOptionPosition }\` so the rejection target is explicit. +When asking the user to reject one specific requirement, include \`requirementReview: { kind: 'requirement-rejection', requirementId, rejectOptionPosition }\` so the rejection target is explicit. When every current requirement has explicit review coverage and the set appears complete for now, use the \`propose_phase_closure\` tool instead of another question. The summary should explain why requirements can close and criteria review can begin. diff --git a/src/shared/chat.ts b/src/shared/chat.ts index 8c3c9d88..cc6609eb 100644 --- a/src/shared/chat.ts +++ b/src/shared/chat.ts @@ -68,16 +68,35 @@ export const structuredQuestionSchema = z ) .min(2), requirementReview: requirementReviewSchema.optional(), + review: requirementReviewSchema.optional(), criterionReview: criterionReviewSchema.optional(), }) .superRefine((value, ctx) => { - if (value.requirementReview) { - validateReviewOptionPosition(value.requirementReview, 'requirementReview', value.options.length, ctx); + if (value.requirementReview && value.review) { + ctx.addIssue({ + code: 'custom', + message: 'Use requirementReview instead of review when both are present', + path: ['review'], + }); + } + + const requirementReview = value.requirementReview ?? value.review; + if (requirementReview) { + validateReviewOptionPosition( + requirementReview, + value.requirementReview ? 'requirementReview' : 'review', + value.options.length, + ctx, + ); } if (value.criterionReview) { validateReviewOptionPosition(value.criterionReview, 'criterionReview', value.options.length, ctx); } - }); + }) + .transform(({ review, requirementReview, ...value }) => ({ + ...value, + ...((requirementReview ?? review) ? { requirementReview: requirementReview ?? review } : {}), + })); export const askQuestionToolOutputSchema = z.object({ ok: z.literal(true), diff --git a/tsconfig.json b/tsconfig.json index 38cd59f6..faf4c1a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "@/*": ["./src/client/*"] } }, - "include": ["src", "node_modules/vite/client.d.ts"] + "include": ["src", ".ladle", "node_modules/vite/client.d.ts"] }