diff --git a/.agents/skills/ln-build/SKILL.md b/.agents/skills/ln-build/SKILL.md index d94218d8..b0deab81 100644 --- a/.agents/skills/ln-build/SKILL.md +++ b/.agents/skills/ln-build/SKILL.md @@ -4,17 +4,17 @@ description: "Implement one scoped slice using TDD red-green-refactor. Use when argument-hint: "[paste or reference a ln-scope card]" --- -# Dev Build +# Ln Build Implement **one** slice. Beck's red-green-refactor, one cycle, no scope creep. ## Input -A scope card from `ln-scope`: $ARGUMENTS +A scope card from `ln-scope`, or one commit-sized step from `memory/REFACTOR.md`: $ARGUMENTS -The canonical path is `ln-scope` → `ln-build`. If no scope card exists, suggest `ln-scope` first. Accept a raw behavior description only for trivial changes where scoping would be ceremony. +The canonical path is `ln-scope` → `ln-build`. For refactors, `ln-refactor` may hand off one commit-sized step to implement. If neither a scope card nor a single refactor step exists, suggest `ln-scope` or `ln-refactor` first. Accept a raw behavior description only for trivial changes where scoping would be ceremony. -Extract: target behavior, boundary crossings, acceptance criteria, and **verification approach**. +Extract: target behavior, boundary crossings, acceptance criteria, and **verification approach**. For refactor steps, derive these from the selected commit step and existing tests before writing new code. ## Red @@ -42,9 +42,9 @@ Run the project's verification harness. All checks must pass. Commit: `feat: [ta After the slice lands and verification passes, do all of these before presenting routing options: -1. Mark the slice `done` in `memory/PLAN.md`. Check `## Dependencies` — if this slice unblocked multiple downstream slices, note them as newly available (some may be parallelizable) -2. Update assumption confidence in `memory/SPEC.md` §Assumptions — set validated assumptions to `**validated**`, invalidated ones to `**invalidated**` and flag implicated slices in PLAN.md -3. Add new invariants to `memory/SPEC.md` §Invariants — each structural property now protected by tests. Update `memory/PLAN.md` slice with `Invariants established: I#` +1. If working from `memory/PLAN.md`, mark the slice `done`. Check `## Dependencies` — if this slice unblocked multiple downstream slices, note them as newly available (some may be parallelizable). If working from `memory/REFACTOR.md`, mark the commit step complete there instead +2. Update `memory/SPEC.md` §Assumptions — set `Status` to `validated` or `invalidated` as evidence warrants, update `Confidence` if the evidence changed it, and flag implicated slices in PLAN.md +3. Add new invariants to `memory/SPEC.md` §Invariants — each structural property now protected by tests. If working from `memory/PLAN.md`, update the `Invariants established` field on the corresponding slice 4. Add any new decisions to `memory/SPEC.md` §Decisions, new assumptions to §Assumptions 5. Update `memory/SPEC.md` §Verification Design → Current Coverage with new test files and counts diff --git a/.agents/skills/ln-consult/SKILL.md b/.agents/skills/ln-consult/SKILL.md index a22454ab..4bd9bc8a 100644 --- a/.agents/skills/ln-consult/SKILL.md +++ b/.agents/skills/ln-consult/SKILL.md @@ -1,14 +1,16 @@ --- name: ln-consult -description: "Lightweight triage for the ln-* skill set. Use when unsure which dev skill to use next, starting work on something new, or when the user asks for guidance on their development process." +description: "Lightweight triage for the ln-* skill set. Use when unsure which ln skill to use next, starting work on something new, or when the user asks for guidance on their development process." --- -# Dev Consult +# Ln Consult Assess where the user is and suggest one `ln-*` skill. If context is unclear, ask **one** clarifying question — then recommend. +Canonical flow is usually `ln-grill → ln-spec → ln-plan → [ln-design when interface shape is uncertain] → [ln-oracles when verification strategy needs explicit design] → ln-scope → [ln-spike] → ln-build → ln-review → [ln-refactor] → [ln-sync]`. + ## Routing table | Situation | Suggest | @@ -16,6 +18,7 @@ If context is unclear, ask **one** clarifying question — then recommend. | Idea is vague, needs fleshing out | `ln-grill` | | Understanding exists, needs a written spec | `ln-spec` | | Spec exists, needs a plan with slices | `ln-plan` | +| Plan/spec exists, needs explicit verification strategy | `ln-oracles` | | Plan exists, next slice needs a scope card | `ln-scope` | | Module interface needs exploration | `ln-design` | | Scope card exists (from `ln-scope`), ready to code | `ln-build` | diff --git a/.agents/skills/ln-design/SKILL.md b/.agents/skills/ln-design/SKILL.md index f0649b52..e7eff992 100644 --- a/.agents/skills/ln-design/SKILL.md +++ b/.agents/skills/ln-design/SKILL.md @@ -4,7 +4,7 @@ description: "Explore radically different module shapes before committing to one argument-hint: "[module or API boundary to explore]" --- -# Dev Design +# Ln Design Apply Ousterhout's "Design It Twice": generate **3+ radically different module shapes**, compare on depth, and synthesize. The goal is deep modules — small API surfaces hiding significant complexity. Do not implement; this is purely about the shape of the boundary. diff --git a/.agents/skills/ln-grill/SKILL.md b/.agents/skills/ln-grill/SKILL.md index 7fbd28bd..1b476abe 100644 --- a/.agents/skills/ln-grill/SKILL.md +++ b/.agents/skills/ln-grill/SKILL.md @@ -3,7 +3,7 @@ name: ln-grill description: "Interview the user relentlessly about a plan or design until reaching shared understanding. Use when fleshing out an idea, stress-testing a design, or when the user says \"grill me\"." --- -# Dev Grill +# Ln Grill Walk the design tree branch by branch. Resolve dependencies between intents/desires/decisions, one by one. Be Socratic — question premises, not just requirements. The user's *why* matters as much or more than their *what*: knowing motivation lets you suggest alternatives they haven't considered. @@ -23,7 +23,7 @@ When understanding is reached, present these options to the user (use `tool-ask- | # | Label | Target | Why | | --- | --------------- | ---------- | --------------------------------------- | -| 1 | Write a spec | `ln-spec` | Understanding is sufficient for a PRD | +| 1 | Write a spec | `ln-spec` | Understanding is sufficient for a spec | | 2 | Plan slices | `ln-plan` | Problem is clear, needs slice breakdown | | 3 | Scope one slice | `ln-scope` | One slice is already obvious | diff --git a/.agents/skills/ln-handoff/SKILL.md b/.agents/skills/ln-handoff/SKILL.md index 9ca26d22..fcb819a9 100644 --- a/.agents/skills/ln-handoff/SKILL.md +++ b/.agents/skills/ln-handoff/SKILL.md @@ -4,7 +4,7 @@ description: "Capture volatile session state into a structured handoff document argument-hint: "[optional: path for handoff file, default HANDOFF.md]" --- -# Dev Handoff +# Ln Handoff Capture what lives in chat but not on disk. Git can reconstruct file changes. But a half-formed scope card, a spike 60% through its investigation, a plan discussion that hasn't hit `memory/PLAN.md` — those are gone on compaction. @@ -17,7 +17,7 @@ The handoff must let a new thread act immediately without asking clarifying ques Which `ln-*` skill was most recently active? Where in the flow is the work? ``` -grill → spec → plan → scope → [spike] → build → review → [sync] +grill → spec → plan → [design] → [oracles] → scope → [spike] → build → review → [refactor] → [sync] ``` Be precise about state: @@ -32,8 +32,11 @@ This is the critical step. Scan the conversation for volatile artifacts — info - **Scope cards** from `ln-scope` — target behavior, boundary crossings, acceptance criteria - **Plan drafts** from `ln-plan` — slice lists, ordering decisions, dependency reasoning not yet in `memory/PLAN.md` +- **Design outputs** from `ln-design` — alternative module shapes considered, the chosen shape, and rejected tradeoffs +- **Oracle design outputs** from `ln-oracles` — O/R/C assessment, selected oracle families, per-slice verification approaches, acknowledged blind spots, and whether slice verification design is complete / pending / stale relative to the code - **Spike state** from `ln-spike` — the question, what was tried, partial findings, verdict if reached - **Review findings** from `ln-review` — **ALL findings, not just the one being acted on.** Review debt is critical context. Name every finding, its status (addressed / in-progress / deferred), and any remaining implications. A fresh thread that only knows about the active finding will lose track of deferred review debt. +- **Refactor state** from `ln-refactor` — commit sequence, target structure, and any constraints on safe ordering - **Grill insights** from `ln-grill` — constraints surfaced, decisions reached - **Decisions and assumptions** discussed but not yet in `memory/SPEC.md` - **Evidence that informed diagnoses** — concrete proof points (API responses, test output, log lines, specific data) that caused the investigation to shift direction or a hypothesis to be confirmed/rejected. Without this, a new thread inherits conclusions but not the reasoning, and may re-investigate or contradict settled evidence. @@ -51,7 +54,7 @@ What IS on disk: ### 4. Produce handoff -Write structured markdown following `@resources/handoff-template.md`. +Write structured markdown following `./assets/handoff-template.md`. Write to the path given as argument, or `HANDOFF.md` at the nearest workspace root. In a monorepo, this is the workspace (package) the session was working in — not the repository root. Determine the workspace from the files touched during the session: look for the nearest `package.json`, `Cargo.toml`, `go.mod`, or similar project marker up from the most-edited files. diff --git a/.agents/skills/ln-handoff/resources/handoff-template.md b/.agents/skills/ln-handoff/assets/handoff-template.md similarity index 84% rename from .agents/skills/ln-handoff/resources/handoff-template.md rename to .agents/skills/ln-handoff/assets/handoff-template.md index f0df9543..e4d0dad9 100644 --- a/.agents/skills/ln-handoff/resources/handoff-template.md +++ b/.agents/skills/ln-handoff/assets/handoff-template.md @@ -13,7 +13,7 @@ - **Last completed skill**: `ln-` — [what it produced] - **Current skill**: `ln-handoff` (or other if handoff is mid-skill) -- **Flow position**: `grill → spec → plan → scope → [spike] → build → review → [sync]` +- **Flow position**: `grill → spec → plan → [design] → [oracles] → scope → [spike] → build → review → [refactor] → [sync]` - **Handoff trigger**: [why the handoff is happening] ## In-flight work @@ -21,8 +21,8 @@ > CRITICAL: These artifacts exist only in the prior conversation, not on disk. > Reproduce them here with full fidelity. -[Scope cards, spike verdicts, plan drafts, grill insights, - decisions — in their native format, not summarized] +[Scope cards, plan drafts, design alternatives, oracle designs (including verification state), spike verdicts, + refactor plans, grill insights, decisions — in their native format, not summarized] ### Review findings @@ -75,7 +75,7 @@ Paste this into a new session: -> Read `HANDOFF.md` in the project root. It contains the full state of in-progress work. +> Read `HANDOFF.md` in the workspace root for this work area. It contains the full state of in-progress work. > The immediate next step is: [first action from Next steps]. > Start by [specific instruction — e.g., "reviewing the scope card in the In-flight section and running ln-build"]. ``` diff --git a/.agents/skills/ln-oracles/SKILL.md b/.agents/skills/ln-oracles/SKILL.md index 0006636a..b56a2740 100644 --- a/.agents/skills/ln-oracles/SKILL.md +++ b/.agents/skills/ln-oracles/SKILL.md @@ -1,16 +1,18 @@ --- name: ln-oracles -description: "Design verification strategy: diagnose observability, select oracle families, map to loop tiers, surface blind spots. Use after ln-plan when slices need oracle design, or when verification coverage has drifted." +description: "Design verification strategy: diagnose observability, select oracle families, map to loop tiers, surface blind spots. Use after ln-plan when slices need oracle design — especially for LLM, visual, or compositional work — or when verification coverage has drifted." argument-hint: "[slices to design oracles for, or 'all' for full reassessment]" --- -# Dev Oracles +# Ln Oracles Design what proves the system works before choosing how to build it. The best oracle removes the most bad degrees of freedom per unit time (Regehr). A system without feedback is open-loop -- it cannot correct errors (Wiener). Verification is first-class work, not accessory: second only to building the product itself. A slice without an oracle strategy is not scoped. -Read `@resources/diagnostic-framework.md` and `@resources/oracle-taxonomy.md` before starting. +Not every slice needs a full oracle-design pass. For trivial, purely structural slices, `ln-scope` may name the inner-loop checks directly. Use `ln-oracles` when the verification strategy itself is uncertain or materially shapes implementation order. + +Read `./assets/diagnostic-framework.md` and `./assets/oracle-taxonomy.md` before starting. ## Input @@ -24,7 +26,7 @@ This is an **interactive process** -- each step involves presenting analysis and ### 1. Diagnose -Score **Observability**, **Reproducibility**, and **Controllability** (see `@resources/diagnostic-framework.md`). Present the scoring table to the user with specific notes per dimension. Low scores constrain which oracle families are feasible and must be addressed before oracle selection proceeds. +Score **Observability**, **Reproducibility**, and **Controllability** (see `./assets/diagnostic-framework.md`). Present the scoring table to the user with specific notes per dimension. Low scores constrain which oracle families are feasible and must be addressed before oracle selection proceeds. **Grill**: For each dimension scored below `high`, ask: is this a deliberate deferral, a blind spot, or something we should address now? What would change the score? @@ -40,7 +42,7 @@ From SPEC.md invariant bundles, acceptance criteria, and PLAN.md slice definitio ### 3. Select oracle families -Using `@resources/oracle-taxonomy.md`, select families ranked by ROI for this project's verification needs. Apply the combination principle: the best oracle is a pair of independent artifacts. Prefer pairs when they compound; don't force them when a single oracle suffices. +Using `./assets/oracle-taxonomy.md`, select families ranked by ROI for this project's verification needs. Apply the combination principle: the best oracle is a pair of independent artifacts. Prefer pairs when they compound; don't force them when a single oracle suffices. **Grill**: For each selected family, present: what it proves, what it costs, and what it misses. Ask the user which tradeoffs are acceptable given timeline and confidence levels. @@ -48,7 +50,7 @@ Using `@resources/oracle-taxonomy.md`, select families ranked by ROI for this pr Assign each selected oracle to inner (ms, agent-autonomous), middle (seconds-minutes, regression/fitness), or outer (slow hardening). Apply verification economics: cheapest checks first, expensive checks less often. -**Boundary with ln-spec**: ln-spec owns the inner loop (verification commands, policy, fast automated checks). ln-oracles owns the middle and outer loops, plus strategic framing (diagnostic, stance, blind spots). When updating, preserve ln-spec's inner loop content and extend with middle/outer strategy. +**Boundary with ln-spec**: ln-spec owns project-wide inner-loop verification commands, policy, and fast automated checks. ln-oracles owns the middle and outer loops, plus strategic framing (diagnostic, stance, blind spots), and may recommend slice-specific inner-loop oracle families when they affect implementation strategy. When updating, preserve ln-spec's command/policy content and extend with middle/outer strategy. **Grill**: For middle-loop oracles that require external resources (API calls, fixtures), ask: how will fixtures be created? What bootstraps ground truth? Is single-shot measurement sufficient or do we need multi-run variance? diff --git a/.agents/skills/ln-oracles/resources/diagnostic-framework.md b/.agents/skills/ln-oracles/assets/diagnostic-framework.md similarity index 100% rename from .agents/skills/ln-oracles/resources/diagnostic-framework.md rename to .agents/skills/ln-oracles/assets/diagnostic-framework.md diff --git a/.agents/skills/ln-oracles/resources/oracle-taxonomy.md b/.agents/skills/ln-oracles/assets/oracle-taxonomy.md similarity index 100% rename from .agents/skills/ln-oracles/resources/oracle-taxonomy.md rename to .agents/skills/ln-oracles/assets/oracle-taxonomy.md diff --git a/.agents/skills/ln-plan/SKILL.md b/.agents/skills/ln-plan/SKILL.md index 57ed30c3..69d50d79 100644 --- a/.agents/skills/ln-plan/SKILL.md +++ b/.agents/skills/ln-plan/SKILL.md @@ -4,7 +4,7 @@ description: "Break a feature or project into vertical slices and update memory/ argument-hint: "[feature or project area to plan]" --- -# Dev Plan +# Ln Plan Break a feature into tracer-bullet slices and spikes (Hunt & Thomas), grouped into temporal phases. Slices are thin end-to-end paths through all integration layers. Order by uncertainty first, dependency second (Reinertsen: retire risk early, not just finish tasks early). @@ -20,28 +20,29 @@ If context is thin, run a brief interview (not a full `ln-grill`) to fill gaps. ## Plan -**Mode detection.** If the user is inserting or reordering specific slices — not replanning from scratch — this is a **patch**. Read PLAN.md, make the targeted edits, then jump to the post-edit checklist (step 5). +**Mode detection.** If the user is inserting or reordering specific slices — not replanning from scratch — this is a **patch**. Read PLAN.md, make the targeted edits, then jump to the post-edit checklist (step 6). 1. If `memory/PLAN.md` exists, read it first. Retire completed slices (mark `done`). Assess what remains and what's changed. 2. Explore the codebase. Identify architectural constraints the slices must respect (routes, schema, auth, third-party boundaries). -3. Draft or revise phases and slices. Each slice must be independently demoable and independently grabbable where possible. Group into temporal phases. For each, name dependent requirements and assumptions from `memory/SPEC.md`. -4. Confirm with user — adjust granularity, reorder, split or merge. -5. **Post-edit checklist** — after any addition, removal, or reordering: +3. Draft or revise phases and slices. Each slice must be independently demoable and independently grabbable where possible. Group into temporal phases. For each, name dependent requirements and assumptions from `memory/SPEC.md`, plus any candidate invariant goals to establish or existing invariants to respect. +4. Observe and respect local project protocols for mapping slices/spikes to issues or tickets, associated codes, and branch naming conventions, if any. Capture project-specific tracking metadata as optional execution detail — not as the core identity of the slice. +5. Confirm with user — adjust granularity, reorder, split or merge. +6. **Post-edit checklist** — after any addition, removal, or reordering: - Update the `## Dependencies` ASCII graph to reflect new/changed edges - Update `### Parallelism opportunities` if new concurrent paths opened - - Verify every new slice names its requirements, assumptions, invariants to establish, and invariants to respect from SPEC.md + - Verify every new slice names its requirements, assumptions, candidate invariant goals, and invariants to respect from SPEC.md ## Output -Write or update `./memory/PLAN.md` following the template at `@resources/plan-template.md`. +Write or update `./memory/PLAN.md` following the template at `./assets/plan-template.md`. ### Traceability -Every slice and spike must name its dependent requirements and assumptions from `memory/SPEC.md`. This is the bridge between the two documents — invalidating an assumption in SPEC surfaces every slice it touches in PLAN. +Every slice and spike must name its dependent requirements and assumptions from `memory/SPEC.md`. Slices should also capture candidate invariant goals to establish or existing invariants to respect, and a verification approach when one is already known. This is the bridge between the two documents — invalidating an assumption in SPEC surfaces every slice it touches in PLAN. ## Routing -After writing the roadmap, present these options to the user (use `tool-ask-question`): +After writing the plan, present these options to the user (use `tool-ask-question`): | # | Label | Target | Why | | --- | ----------------- | ------------ | ----------------------------------------------- | @@ -52,4 +53,4 @@ After writing the roadmap, present these options to the user (use `tool-ask-ques Recommended: **1** --- -*Draws from [mattpocock/skills/prd-to-plan](https://github.com/mattpocock/skills/tree/main/prd-to-plan) and [mattpocock/skills/prd-to-issues](https://github.com/mattpocock/skills/tree/main/prd-to-issues).* +*Draws from [mattpocock/skills/prd-to-plan](https://github.com/mattpocock/skills/tree/main/prd-to-plan) and [mattpocock/skills/prd-to-issues](https://github.com/mattpocock/skills/tree/main/prd-to-issues), adapted toward a generic PLAN.md workflow rather than project-specific issue/branch bindings.* diff --git a/.agents/skills/ln-plan/resources/plan-template.md b/.agents/skills/ln-plan/assets/plan-template.md similarity index 52% rename from .agents/skills/ln-plan/resources/plan-template.md rename to .agents/skills/ln-plan/assets/plan-template.md index 8e5b0a09..97bcc242 100644 --- a/.agents/skills/ln-plan/resources/plan-template.md +++ b/.agents/skills/ln-plan/assets/plan-template.md @@ -1,10 +1,12 @@ + Invalidating an assumption in SPEC surfaces every slice it touches here. + Respect local project protocols for issue/ticket mapping and branch naming, if any, + but keep that metadata optional and secondary to the slice itself. --> # Plan @@ -18,17 +20,22 @@ ### Slices -1. **[Slice name]** `[ISSUE-ID]` — [why this, why now] `[status: not-started|in-progress|done]` +1. **[Slice name]** — [why this, why now] `[status: not-started|in-progress|done]` - Requirements: [→ SPEC.md §Requirements #N, #N] - Assumptions: [→ SPEC.md §Assumptions A1, A2] + - Candidate invariant goals: [structural property this slice should establish; assign I# after build/spike if proven] + - Invariants to respect: [→ SPEC.md §Invariants I#, I# | none] - Acceptance: [observable, testable target] - - Branch: `[branch-name]` + - Verification approach: [inner/middle/outer oracle family summary, or `to be designed`] + - Invariants established: [I# | none yet] + - Execution tracking (optional): [issue/ticket code, branch name, or other local protocol metadata] ### Spikes -1. **[Spike name]** `[ISSUE-ID]` — [question to answer] `[status: not-started|in-progress|done]` +1. **[Spike name]** — [question to answer] `[status: not-started|in-progress|done]` - Assumptions: [→ SPEC.md §Assumptions being tested] - - Branch: `[branch-name]` + - Decision unlocked: [what this spike informs] + - Execution tracking (optional): [issue/ticket code, branch name, or other local protocol metadata] ## Phase 2: [name] ... @@ -40,3 +47,7 @@ ``` [ASCII diagram of blocking relationships] ``` + +### Parallelism opportunities + +- [Slices that are currently unblocked and can proceed concurrently] diff --git a/.agents/skills/ln-refactor/SKILL.md b/.agents/skills/ln-refactor/SKILL.md index 36ebebbc..564a0f6d 100644 --- a/.agents/skills/ln-refactor/SKILL.md +++ b/.agents/skills/ln-refactor/SKILL.md @@ -4,7 +4,7 @@ description: "Plan a refactor as a sequence of tiny safe commits via interview a argument-hint: "[area or module to refactor]" --- -# Dev Refactor +# Ln Refactor "Make the change easy, then make the easy change" (Beck). Plan a refactor as tiny commits that each leave the codebase working (Fowler). Preparatory refactoring first, behavioral changes last. @@ -66,13 +66,13 @@ What this refactor deliberately excludes. After filing the refactor plan, present these options to the user (use `tool-ask-question`): -| # | Label | Target | Why | -| --- | ------------------ | ------------ | ------------------------------------------ | -| 1 | Build first commit | `ln-build` | Refactor plan is clear, start implementing | -| 2 | Scope a commit | `ln-scope` | A commit needs more precise definition | -| 3 | Back to triage | `ln-consult` | Plan needs reassessment | +| # | Label | Target | Why | +| --- | ------------------ | ------------ | ------------------------------------------------------------ | +| 1 | Build first commit | `ln-build` | Refactor plan is clear; implement one commit-sized step from `memory/REFACTOR.md` | +| 2 | Scope a commit | `ln-scope` | A commit needs more precise behavior/acceptance definition | +| 3 | Back to triage | `ln-consult` | Plan needs reassessment | -Recommended: **1** +Recommended: **1** when the first commit step is concrete enough to execute; otherwise **2** --- *Adapted from [mattpocock/skills/request-refactor-plan](https://github.com/mattpocock/skills/tree/main/request-refactor-plan).* diff --git a/.agents/skills/ln-review/SKILL.md b/.agents/skills/ln-review/SKILL.md index 1634a2dd..cbda2f0d 100644 --- a/.agents/skills/ln-review/SKILL.md +++ b/.agents/skills/ln-review/SKILL.md @@ -4,7 +4,7 @@ description: "Audit code quality focusing on deep modules, naming, model hygiene argument-hint: "[area of codebase to review, or 'recent' for recently changed files]" --- -# Dev Review +# Ln Review Explore the codebase. Surface structural improvement opportunities. Be opinionated. @@ -24,7 +24,7 @@ Make invalid states unrepresentable (Yaron Minsky). Split optional fields into d ### Oracle coverage -If `memory/SPEC.md` §Oracle Strategy by Loop Tier exists, check whether recent slices implemented the oracles their scope cards declared. Look for: +If `memory/SPEC.md` §Oracle Strategy by Loop Tier exists, check whether recent slices implemented the oracles their persisted `memory/PLAN.md` verification approaches declare. If a scope card is available in session context, use it as a higher-resolution supplement, not the primary source of truth. Look for: - Scope card promised schema validation → is there a Zod parse in the test? - Scope card promised differential oracle → are there golden master fixtures? diff --git a/.agents/skills/ln-scope/SKILL.md b/.agents/skills/ln-scope/SKILL.md index fa758836..bd4c32b0 100644 --- a/.agents/skills/ln-scope/SKILL.md +++ b/.agents/skills/ln-scope/SKILL.md @@ -4,7 +4,7 @@ description: "Define one thin vertical slice with target behavior, risks, and ac argument-hint: "[behavior to deliver in this slice]" --- -# Dev Scope +# Ln Scope Define **one** tracer-bullet slice (Hunt & Thomas) — a thin end-to-end path, not a horizontal layer. If the target behavior needs "and", split it. @@ -51,7 +51,7 @@ These become the spec tests written first in `ln-build`. Every criterion must be ### Verification Approach -Name the oracle strategy for this slice. If `memory/SPEC.md` §Oracle Strategy by Loop Tier exists, pick from the families already selected. If it doesn't, suggest running `ln-oracles` first. +Name the oracle strategy for this slice. If `memory/SPEC.md` §Oracle Strategy by Loop Tier exists, pick from the families already selected. If it doesn't, suggest running `ln-oracles` first unless the slice is trivial and purely structural, in which case naming the inner-loop checks directly may be sufficient. ``` - Inner: [oracle family] — [what it proves] @@ -59,7 +59,7 @@ Name the oracle strategy for this slice. If `memory/SPEC.md` §Oracle Strategy b - Outer: [oracle family] — [what it proves] (if applicable) ``` -A slice without a verification approach is not fully scoped. At minimum, inner-loop oracles must be named. Middle/outer are required when the slice touches LLM boundaries, visual rendering, or compositional behavior. +A slice without a verification approach is not fully scoped. At minimum, inner-loop oracles must be named. Middle/outer are required when the slice touches LLM boundaries, visual rendering, or compositional behavior. Those slices should run through `ln-oracles` before `ln-build`. ## Traceability (mandatory — do before routing) @@ -71,12 +71,13 @@ After the scope card is complete, do these before presenting routing options: After traceability is complete, present these options to the user (use `tool-ask-question`): -| # | Label | Target | Why | -| --- | -------------- | ------------ | ----------------------------------------------- | -| 1 | Build it | `ln-build` | Slice is defined, ready to implement | -| 2 | Spike first | `ln-spike` | Technical uncertainty needs resolution | -| 3 | Revise spec | `ln-spec` | Scoping revealed the spec needs structural revision | -| 4 | Revise plan | `ln-plan` | Slice doesn't fit the current plan | -| 5 | Back to triage | `ln-consult` | Scope revealed unclear state | +| # | Label | Target | Why | +| --- | -------------- | ------------ | ---------------------------------------------------- | +| 1 | Build it | `ln-build` | Slice is defined and its verification strategy exists | +| 2 | Design oracles | `ln-oracles` | Slice needs explicit oracle design before implementation | +| 3 | Spike first | `ln-spike` | Technical uncertainty needs resolution | +| 4 | Revise spec | `ln-spec` | Scoping revealed the spec needs structural revision | +| 5 | Revise plan | `ln-plan` | Slice doesn't fit the current plan | +| 6 | Back to triage | `ln-consult` | Scope revealed unclear state | -Recommended: **1** unless risks flagged a spike. +Recommended: **2** if the slice lacks oracle strategy and is not trivial/purely structural; otherwise **1** unless risks flagged a spike. diff --git a/.agents/skills/ln-spec/SKILL.md b/.agents/skills/ln-spec/SKILL.md index d0aa43d9..4820b974 100644 --- a/.agents/skills/ln-spec/SKILL.md +++ b/.agents/skills/ln-spec/SKILL.md @@ -4,7 +4,7 @@ description: "Crystallize shared understanding into a reviewable spec, or update argument-hint: "[feature or problem to specify]" --- -# Dev Spec +# Ln Spec Crystallize understanding into a **spec** — the reviewable decision record between shared agreement and actionable plan. Every section should close a decision; a spec that restates the conversation instead of narrowing the solution space has failed. @@ -20,29 +20,33 @@ The feature or problem: $ARGUMENTS 1. **Capture the problem** from the user's perspective — what they want and *why*. The *why* shapes the solution space. 2. **Explore the codebase** to verify assertions, understand current state, and find existing patterns. If `memory/SPEC.md` exists, read it first — this is an update, not a blank-slate write. -3. **Interview** (if understanding is thin), to close remaining gaps. Walk each branch of the design tree. For each question, provide your recommended answer. If the codebase can answer a question, explore it instead of asking. Use `/ln-grill` if it hasn't already been run. -4. **Sketch modules** to build or modify. Apply Ousterhout's depth test — favor deep modules with small interfaces and large hidden implementations, testable in isolation. Check with the user that modules match expectations. Use `/ln-design` if it hasn't already been run. +3. **Interview** (if understanding is thin), to close remaining gaps. Walk each branch of the design tree. For each question, provide your recommended answer. If the codebase can answer a question, explore it instead of asking. Use `ln-grill` if it hasn't already been run. +4. **Sketch modules** to build or modify. Apply Ousterhout's depth test — favor deep modules with small interfaces and large hidden implementations, testable in isolation. Check with the user that modules match expectations. Use `ln-design` if it hasn't already been run. 5. **Write or update** `./memory/SPEC.md`. ## Output -Write or update `./memory/SPEC.md` following the template at `@resources/spec-template.md`. If the file already exists, read it first — preserve existing content, evolve sections that need change. +Write or update `./memory/SPEC.md` following the template at `./assets/spec-template.md`. If the file already exists, read it first — preserve existing content, evolve sections that need change. ### Verification Design boundary -ln-spec owns the **inner loop** of verification design: verification commands, verification policy, and inner-loop oracle items (type checks, fast unit tests, linting). Middle and outer loop oracle strategy, diagnostic assessment, and blind spots are owned by `ln-oracles`. When writing or updating §Verification Design, preserve any content written by ln-oracles (§Verification Stance, §Diagnostic Assessment, §Oracle Strategy middle/outer tiers, §Design notes, §Acknowledged Blind Spots). +ln-spec owns the **inner loop** of verification design: verification commands, verification policy, and inner-loop oracle items (type checks, fast unit tests, linting). Middle and outer loop oracle strategy, diagnostic assessment, and blind spots are owned by `ln-oracles`. Not every slice requires a full oracle-design pass, but slices involving LLM behavior, visual rendering, or compositional/system-level claims should route through `ln-oracles` before implementation. When writing or updating §Verification Design, preserve any content written by ln-oracles (§Verification Stance, §Diagnostic Assessment, §Oracle Strategy middle/outer tiers, §Design notes, §Acknowledged Blind Spots). ### Traceability -If `memory/PLAN.md` exists, verify that changed assumptions and decisions still align with affected slices. +If `memory/PLAN.md` exists, verify that changed assumptions and decisions still align with affected slices. If it does not exist yet, close the reference chain as far as current artifacts allow: assumptions should still name dependent decisions and validation approaches, and slice links can be added later by `ln-plan`. + +### Weight management + +Spec documents accumulate. Each ln-sync pass may prune items that are embedded, moot, or superseded (see ln-sync §Pruning check). When *adding* items, consider whether an existing item should be retired to make room. A spec with 30 assumptions is not more rigorous than one with 10 — it's harder to read and more likely to mislead a new session. ### Cross-reference integrity -Every amendment must close its reference chain. After editing, verify: +Every amendment must close its reference chain as far as the current lifecycle stage allows. After editing, verify: -- **New assumption** → has: dependent decision(s), implicated slice(s) in PLAN.md, validation approach +- **New assumption** → has: dependent decision(s), validation approach, and implicated slice(s) in PLAN.md **if PLAN.md already exists** - **New decision** → has: dependent assumption(s), supersession note -- **New invariant** → has: establishing slice in PLAN.md, protecting test (or `manual (outer loop)`), proved decision +- **New invariant** → has: establishing slice in PLAN.md **if known**, protecting test (or `manual (outer loop)`), proved decision - **New constraint** → has: rationale for exclusion - **New inner-loop oracle item** → names the invariant(s) it protects diff --git a/.agents/skills/ln-spec/resources/spec-template.md b/.agents/skills/ln-spec/assets/spec-template.md similarity index 87% rename from .agents/skills/ln-spec/resources/spec-template.md rename to .agents/skills/ln-spec/assets/spec-template.md index b4ffb8d3..04864ed6 100644 --- a/.agents/skills/ln-spec/resources/spec-template.md +++ b/.agents/skills/ln-spec/assets/spec-template.md @@ -1,5 +1,5 @@ -| # | Assumption | Confidence | Dependent decisions | Implicated slices | Validation approach | -| --- | ------------ | --------------- | ------------------- | ----------------- | ------------------- | -| A1 | [hypothesis] | low/medium/high | [→ §Decisions #N] | [→ PLAN.md slice] | [how to falsify] | +| # | Assumption | Confidence | Status | Dependent decisions | Implicated slices | Validation approach | +| --- | ------------ | --------------- | -------------------------- | ------------------- | ----------------- | ------------------- | +| A1 | [hypothesis] | low/medium/high | open/validated/invalidated | [→ §Decisions #N] | [→ PLAN.md slice] | [how to falsify] | ## Decisions @@ -63,7 +65,7 @@ | Term | Definition | | --------------- | --------------------------------------------------------------------------------------------- | -| **assumption** | A falsifiable belief accepted as true; tracked with confidence, linked to decisions and slices | +| **assumption** | A falsifiable belief accepted as true; tracked with confidence and status, linked to decisions and slices | | **decision** | A recorded choice that resolves a question; ordered, with supersession chain | | **invariant** | A structural property proven by implementation and protected by tests; must not regress | | **requirement** | A capability the system must provide | diff --git a/.agents/skills/ln-spike/SKILL.md b/.agents/skills/ln-spike/SKILL.md index 3fda48e5..0b04cefd 100644 --- a/.agents/skills/ln-spike/SKILL.md +++ b/.agents/skills/ln-spike/SKILL.md @@ -4,7 +4,7 @@ description: "Time-boxed throwaway investigation to answer one hard question. Us argument-hint: "[question to answer and what you'll try]" --- -# Dev Spike +# Ln Spike Retire one uncertainty. Output is knowledge, not code — spike code is throwaway, never promoted directly (Beck, XP). One question per spike; if multiple unknowns exist, run multiple spikes. @@ -39,7 +39,7 @@ The question and approach: $ARGUMENTS After the verdict, do all of these before presenting routing options: 1. Mark the spike `done` in `memory/PLAN.md` -2. Update assumption confidence in `memory/SPEC.md` §Assumptions — set validated to `**validated**`, invalidated to `**invalidated**` and flag implicated slices in PLAN.md +2. Update `memory/SPEC.md` §Assumptions — set `Status` to `validated` or `invalidated` as evidence warrants, update `Confidence` if the evidence changed it, and flag implicated slices in PLAN.md 3. Add any new decisions to `memory/SPEC.md` §Decisions, new assumptions to §Assumptions 4. If the verdict changes slice feasibility → update affected slices in `memory/PLAN.md` @@ -57,4 +57,4 @@ After traceability is complete, present these options to the user (use `tool-ask | 4 | Revise plan | `ln-plan` | Verdict changes what slices are needed | | 5 | Back to triage | `ln-consult` | Verdict changes the overall direction | -Recommended: **1** if the spike validated, **3** if it invalidated. +Recommended: **1** if the spike validated. If it invalidated an architectural or requirement-level assumption, prefer **3**; if it mainly changes slice feasibility, ordering, or dependencies, prefer **4**. diff --git a/.agents/skills/ln-sync/SKILL.md b/.agents/skills/ln-sync/SKILL.md index ec65ee25..cfa215b5 100644 --- a/.agents/skills/ln-sync/SKILL.md +++ b/.agents/skills/ln-sync/SKILL.md @@ -3,7 +3,7 @@ name: ln-sync description: "Refresh memory/SPEC.md and memory/PLAN.md — graduate assumptions, archive stale items, and flag drift between docs and code. Use periodically or when docs feel out of date." --- -# Dev Sync +# Ln Sync Audit and refresh the two project documents. Create any that are missing. @@ -22,16 +22,32 @@ If either is missing, prompt `ln-spec` or `ln-plan` to create it. ### 2. Graduation check -For each validated assumption in `memory/SPEC.md` §Assumptions: +For each assumption in `memory/SPEC.md` §Assumptions whose `Status` is `validated`: - If it establishes a durable truth → promote to §Lexicon as an invariant or term - If it resolved a choice → promote to §Decisions -- Mark as `validated` in §Assumptions +- Preserve `Status: validated` in §Assumptions -For each invalidated assumption: +For each assumption whose `Status` is `invalidated`: - If it led to an alternative choice → record in §Decisions (superseding the prior decision) -- Mark as `invalidated` in §Assumptions +- Preserve `Status: invalidated` in §Assumptions - Flag all implicated slices in `memory/PLAN.md` +### 2b. Pruning check + +Tracked items accumulate. Large assumption and decision tables become a confusion surface — new sessions inherit stale context and make wrong inferences. After graduation, assess each remaining item for removal: + +| State | Criterion | Action | +| --- | --- | --- | +| **Embedded** | Status `validated`, and now a structural property of the code — restating it as a tracked question adds noise, not clarity | Remove — the code is the proof | +| **Moot** | Status `invalidated`, and the concern no longer applies (e.g. the technology it worried about was replaced entirely) | Remove | +| **Superseded** | Replaced by a newer decision or assumption | Remove, note in the replacement | + +When pruning, leave a comment noting which IDs were removed and why (e.g. ``). Do not renumber surviving items — IDs are stable. Dereference removed items from PLAN.md slice cross-references. + +After pruning, repair or replace any dangling cross-references in `memory/SPEC.md` and `memory/PLAN.md` that pointed at removed assumptions, decisions, invariants, or verification notes. + +The same logic applies to §Decisions: a decision that is now simply how the code works, with no live alternative being weighed, can be removed. Keep decisions that record a *choice between alternatives* that future work might revisit. + ### 3. Staleness check - **PLAN.md**: Are completed slices still marked in-progress? Are active items still relevant? Do slice/spike cross-references to SPEC.md §Requirements and §Assumptions still hold? @@ -55,6 +71,9 @@ Present findings, then update docs with user confirmation: ### Graduations - [assumption] → [promoted to Lexicon/Design Decisions] +### Pruned +- [items removed as embedded/moot/superseded, with rationale] + ### Stale items - [item] in [file] — [what's wrong] diff --git a/.augment/skills b/.augment/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.augment/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.augment/skills/ai-elements b/.augment/skills/ai-elements deleted file mode 120000 index ccc122f6..00000000 --- a/.augment/skills/ai-elements +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/ai-elements \ No newline at end of file diff --git a/.gitignore b/.gitignore index e5c27532..8ae32b63 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ todo.txt # Claude Code worktrees .claude/worktrees/ +tmp/ + +# codetours +.tours/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..5a699e20 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": [ + "shadcn@latest", + "mcp" + ] + }, + "forge_extension": { + "url": "http://localhost:58459/mcp" + } + } +} \ No newline at end of file diff --git a/.tours/2-client-refactor-overview.tour b/.tours/2-client-refactor-overview.tour new file mode 100644 index 00000000..df08d9e2 --- /dev/null +++ b/.tours/2-client-refactor-overview.tour @@ -0,0 +1,48 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "2 - Client Refactor Overview", + "description": "Guided walkthrough of the client refactor that separated progressive rendering, workspace state ownership, mutation choreography, and seam-level verification.", + "nextTour": "3 - Progressive Rendering Boundaries", + "steps": [ + { + "title": "Why This Refactor Exists", + "description": "This series explains the client refactor that followed the AI SDK chat pivot. The goal was not feature expansion. It was to make the default interview path cheaper to load, make workspace ownership legible, move write choreography out of route components, and protect the new seams with direct oracles. Read this tour first for the map, then continue into [3 - Progressive Rendering Boundaries#1] and [4 - Workspace Controller And Mutation Boundaries#1] for the subsystem details." + }, + { + "title": "The Route Map Keeps Optional Surfaces Off The Critical Path", + "file": "src/client/router.tsx", + "pattern": "^const debugRoute = createRoute\\(", + "description": "Start here because the router shows the product-level shape of the refactor. The main interview route stays direct, but `/debug` now points at a lazy boundary instead of eagerly importing the developer surface. That preserves discoverability while making the default app path pay only for the interview shell." + }, + { + "title": "Capability Boundaries Became Architectural Seams", + "file": "src/client/capability-boundaries.test.ts", + "pattern": "^describe\\('client capability boundaries', \\(\\) => \\{$", + "description": "This source-level oracle is the architectural inventory for the refactor. It asserts that markdown, reasoning, code highlighting, and debug UI are all reached through named boundaries rather than direct vendor imports. The rationale is depth: later performance work can change loading behavior in one place without another wide mechanical rewrite." + }, + { + "title": "Progressive Rendering Is A Deliberate Boundary, Not An Accident", + "file": "src/client/capabilities/markdown-rendering.tsx", + "pattern": "^export const MarkdownRenderer = \\({ children, \.\.\.props }: MarkdownRendererProps\\) => \\{$", + "description": "The transcript now reads through a single markdown boundary that decides when rich rendering is actually warranted. This module owns the plain-first path, the lazy rich renderer, and the new intent-warming hook for likely-next enhancement. The rationale is that text stays cheap and immediate, while heavy rendering becomes an explicit policy decision instead of a baseline import cost." + }, + { + "title": "Workspace Behavior Collapsed Behind One Primary Client Boundary", + "file": "src/client/workspace/workspace-controller.ts", + "pattern": "^export function useWorkspaceController\\(\\): WorkspaceController \\{$", + "description": "The interview route used to choreograph loader data, hydration policy, observer invalidation, and option-selection follow-through inline. Now the route reads one controller boundary and mostly renders. The controller itself is still layered internally, but from the outside it is the primary client mechanism for workspace ownership. Continue in [4 - Workspace Controller And Mutation Boundaries#1] for the shell/core split." + }, + { + "title": "Mutation Choreography Moved Out Of Routes", + "file": "src/client/mutations/project-mutations.ts", + "pattern": "^export function useCreateProjectMutation\\(\\) \\{$", + "description": "Project creation and workspace option selection used to depend directly on the generic transport helper plus route-local success choreography. The refactor keeps `client-mutation.ts` as the shared transport/error seam, but adds small domain hooks that own navigation, invalidation, and follow-through. That makes the routes read like rendering shells instead of mini state machines." + }, + { + "title": "Verification Ends The Story, Not Just The Code", + "file": "src/client/build-boundary.test.ts", + "pattern": "^describe\\('client build boundary', \\(\\) => \\{$", + "description": "End with the oracle because it captures the architectural promise in executable form. The refactor is not complete just because the code lazy-loads — the test also checks that debug, rich markdown, and highlighter code stay out of the default entry chunk, and that the minified entry remains under an explicit size ceiling. For the behavioral seams beneath the route level, see [4 - Workspace Controller And Mutation Boundaries#6]. Run >> npm run verify to confirm the full story." + } + ] +} diff --git a/.tours/3-progressive-rendering-boundaries.tour b/.tours/3-progressive-rendering-boundaries.tour new file mode 100644 index 00000000..e63bd08e --- /dev/null +++ b/.tours/3-progressive-rendering-boundaries.tour @@ -0,0 +1,48 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "3 - Progressive Rendering Boundaries", + "description": "How the client refactor turned markdown, code highlighting, and debug UI into explicit progressive-enhancement and performance boundaries.", + "nextTour": "4 - Workspace Controller And Mutation Boundaries", + "steps": [ + { + "title": "Why This Subsystem Changed", + "description": "This tour covers the rendering half of the client refactor: commits 2, 3, 4, 9, and the final closeout of 10. The design goal was to keep the transcript text-first on first paint, move optional rich rendering behind named boundaries, and add both intent warming and build-time guardrails so future feature work cannot silently pull heavy dependencies back into the critical path." + }, + { + "title": "Markdown Owns The Enhancement Decision", + "file": "src/client/capabilities/markdown-rendering.tsx", + "pattern": "^export const needsRichMarkdownRendering = \\(content: string\\) =>$", + "description": "This file is the front door for transcript rendering. `needsRichMarkdownRendering()` decides when plain text is enough, `preloadRichMarkdownRenderer()` exposes an explicit warm-up surface, and `MarkdownRenderer` delays rich upgrade while a message is still animating. The rationale is subtle but important: the default path should stay cheap and visually stable even when the rich path exists." + }, + { + "title": "Code Highlighting Is Split Into Sync Fallback, Async Upgrade, And Warm-Up", + "file": "src/client/capabilities/code-highlighting.ts", + "pattern": "^export const preloadRichCodeHighlighter = \\(\\) => loadRichCodeHighlighter\\(\\);$", + "description": "The highlighting boundary mirrors the markdown one. Plain tokenization is synchronous and always available, rich Shiki-backed highlighting stays behind a lazy import, and `preloadRichCodeHighlighter()` lets the UI warm the expensive runtime before the user fully commits to it. This is the refactor's 'make the lazy seam also preloadable' step." + }, + { + "title": "The Code Block Turns User Intent Into Preloading", + "file": "src/client/components/ai-elements/code-block.tsx", + "pattern": "^export const CodeBlockContainer = \\({$", + "description": "This is where the preload hook becomes user-visible behavior. The code block container warms the rich highlighter on pointer, focus, and touch intent, while `CodeBlockContent` still renders plain tokens immediately and upgrades asynchronously. The rationale is that perceived performance should improve when the user signals likely interaction, without making initial render any heavier." + }, + { + "title": "Message And Reasoning Components Stay Shallow By Delegating Rendering Policy", + "file": "src/client/components/ai-elements/message.tsx", + "pattern": "^export const MessageResponse = memo\\($", + "description": "The transcript components intentionally do very little now. `MessageResponse` hands rendering policy to `MarkdownRenderer`, and `reasoning.tsx` follows the same pattern through `ReasoningRenderer`. This is the payoff of the capability boundary extraction: feature components stay focused on layout and state, while rendering strategy lives in dedicated modules." + }, + { + "title": "The Build Oracle Enforces Shape And Budget", + "file": "src/client/build-boundary.test.ts", + "pattern": "^ it\\('keeps debug and rich markdown rendering out of the default client entrypoint', async \\(\\) => \\{$", + "description": "The final performance guardrail lives here. The test checks chunk shape in a readable build, then runs a minified build and enforces an explicit size ceiling on the default entry file. That means part 10 is now protected as a policy, not just an intention written in a refactor plan." + }, + { + "title": "The Tests Capture The User-Facing Promise", + "file": "src/client/capabilities/markdown-rendering.test.tsx", + "pattern": "^describe\\('MarkdownRenderer', \\(\\) => \\{$", + "description": "Finish with the focused oracles. `markdown-rendering.test.tsx` protects plain-first rendering and the animating-message guard, while [code-block.test.tsx](./src/client/components/ai-elements/code-block.test.tsx) protects stale-result handling, timer cleanup, and intent-triggered preloading. If you are extending the rendering system, keep these tests aligned with the architectural promise before changing implementation details." + } + ] +} diff --git a/.tours/4-workspace-controller-and-mutation-boundaries.tour b/.tours/4-workspace-controller-and-mutation-boundaries.tour new file mode 100644 index 00000000..105d45fc --- /dev/null +++ b/.tours/4-workspace-controller-and-mutation-boundaries.tour @@ -0,0 +1,47 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "4 - Workspace Controller And Mutation Boundaries", + "description": "How the client refactor concentrated workspace ownership into a controller boundary, split pure projection from React shells, and moved write choreography into domain hooks with seam-level tests.", + "steps": [ + { + "title": "Why The Workspace Path Needed Consolidation", + "description": "This tour covers commits 5 through 14 on the workspace side. The original route owned loader seeding, entity refetching, transcript hydration rules, and option-selection follow-through directly. The refactor breaks that apart into a pure projection core, thin query/chat shells, and domain-shaped mutation hooks, then adds direct seam tests so future route cleanups do not have to pay the full component-mock tax." + }, + { + "title": "The Pure Core Shapes Durable And Ephemeral State", + "file": "src/client/workspace/workspace-controller-core.ts", + "pattern": "^export function createWorkspaceDurableProjectState\\(projectState: ProjectState\\): WorkspaceDurableProjectState \\{$", + "description": "Start here because this file is the functional core the later commits were aiming at. It reconstructs durable project state, durable entity state, and ephemeral chat seeds from persisted data, projects controller view state, and performs small pure lookups like `findTurnOptionByPosition()`. The rationale is Bernhardt's boundary rule: state shaping and transcript reconstruction should not live in the same module as React Query and router side effects." + }, + { + "title": "The Durable Workspace Shell Owns Query State", + "file": "src/client/workspace/workspace-data.ts", + "pattern": "^export function useWorkspaceDataAdapter\\($", + "description": "This hook is now the imperative shell around the pure core. It seeds the entity cache from loader data, owns the durable entity query, and bridges `data-observer-result` into query invalidation. The important reading lens is that this file no longer decides what workspace state means — it only decides how React Query acquires and refreshes it." + }, + { + "title": "Hydration Policy Is A Standalone Mechanism", + "file": "src/client/workspace/chat-hydration.ts", + "pattern": "^export function useChatHydrationBoundary\\($", + "description": "The transcript reset rule was pulled out into its own boundary so it could become an explicit policy instead of an effect-dependency accident. `getChatHydrationReason()` names the allowed cases — initial project entry and project navigation — while same-project refreshes intentionally avoid rewriting the visible transcript. That policy is one of the key user-visible invariants the refactor protects." + }, + { + "title": "The Controller Turns Several Shells Into One Route-Facing Interface", + "file": "src/client/workspace/workspace-controller.ts", + "pattern": "^export function useWorkspaceController\\(\\): WorkspaceController \\{$", + "description": "This is the route-facing deep module created by the depth pass. It composes the durable workspace shell, the hydration boundary, `useChat`, view-state projection, and mutation hooks into one interface the route can mostly render. The route now asks for `project`, `chat`, `turnCard`, and `promptInput` instead of orchestrating them itself." + }, + { + "title": "Domain Hooks Hide Write Choreography", + "file": "src/client/mutations/workspace-mutations.ts", + "pattern": "^export function useSelectTurnOptionMutation\\({$", + "description": "This step covers both mutation modules. `useSelectTurnOptionMutation()` owns select → invalidate → echo-selected-text, while [project-mutations.ts](./src/client/mutations/project-mutations.ts) owns create → navigate. The shared `client-mutation.ts` helper still defines the transport/error seam, but callers no longer have to repeat the domain follow-through that makes the action meaningful." + }, + { + "title": "Seam Tests Now Protect The Deep Modules Directly", + "file": "src/client/workspace/workspace-controller.test.tsx", + "pattern": "^describe\\('workspace controller', \\(\\) => \\{$", + "description": "End with the new oracles. `workspace-controller.test.tsx` proves loader-seeded state and same-project refresh behavior at the controller boundary, while [client-mutation.test.ts](./src/client/mutations/client-mutation.test.ts) proves network failure, non-JSON failure, and malformed success handling at the transport seam. That is the architectural payoff of the depth pass: the most important behavior is now protected below the route component itself. Run >> npm run verify if you want to validate the whole refactor before extending it." + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 3e68225d..53325519 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,3 +68,4 @@ Read these before the relevant activity: - **`docs/praxis/graphite-workflow.md`** — before creating branches, submitting PRs, or reintegrating parallel work - **`docs/praxis/worktree-agents.md`** — before spawning parallel agent builds with `isolation: "worktree"` - **`docs/praxis/manual-testing.md`** — before outer-loop UI testing or fixture capture +- **`docs/praxis/dev-server-logs.md`** — before reading runtime logs from the dev server or browser diff --git a/README.md b/README.md index bf4fe144..131768d3 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,109 @@ # Brunch — AI Spec Elicitation -Brunch is a prototype tool that turns natural-language project goals into structured specifications through an AI-guided workflow. A user describes what they want to build, and the system walks them through clarifying questions, assumption review, and hierarchical requirement generation — producing a spec document at each stage. +Brunch is an AI-guided spec elicitation tool that turns natural-language project goals into structured specifications through a multi-phase interview. An interviewer agent asks structured questions — each with options, a recommendation, and strategic grounding — while a separate observer agent extracts decisions and assumptions, building a dependency graph. -Built as a trial project at HASH. The stack is **Preact + Vite** (frontend), **Express.js** (backend), **Dolt** (MySQL-compatible database with git-like version control), and **Anthropic Claude Agent SDK** as the primary AI backend. +Built as a trial project at HASH. The stack is **React 19 + Vite** (frontend), **Express.js** (backend), **SQLite via Drizzle ORM** (database), and **Vercel AI SDK + Anthropic Claude** (AI). -## What was built - -### Two frontend versions - -There are two versions of the UI, both functional: - -1. **Home** (`/`, `/session/:id`) — the original, more feature-complete interface. Includes: - - Goal input with example prompts and AI assistant fallback for weak prompts - - Clarifying questions flow - - Assumption review with inline AI editing - - Hierarchical requirements with sub-requirements and test/check generation - - Three requirement views: list, table, and canvas with drag-and-drop reordering/nesting - - Dolt version control sidebar (commit, history, diff, checkout, revert) - - LLM call log viewer (model, tokens, duration, prompt/response) - - Model selector (switch models per query) - -2. **CreateSpec wizard** (`/create-spec/:projectId/:step`) — a redesign adhering to Figma designs. Includes: - - Step-by-step wizard navigation with progress sidebar - - Clarifying questions with single-choice and multi-choice support - - Cleaner assumption editing UX - - Inline AI assistant panel - - Spec regenerated after each step - -### Backend - -- Express.js API with streaming (NDJSON) responses -- Multi-provider AI dispatch: Claude models use Anthropic SDK directly, other models route through OpenCode -- MCP (Model Context Protocol) tool integration — the AI assistant can call tools to mutate goals, assumptions, and requirements -- Structured output with JSON Schema validation for AI responses -- Full request/response logging to database (`api_call` and `claude_call` tables) -- Automatic database migration on startup - -### Database - -Dolt (MySQL-compatible) with 6 tables: -- `project` — session metadata, goal, clarifying state, spec content -- `entry` — hierarchical requirement tree (self-referencing via `parent_id`) -- `assumption` — normalized assumptions with status tracking -- `goal_iteration` — goal refinement history -- `api_call` — HTTP request/response log -- `claude_call` — LLM interaction log with token counts - -Dolt's git-like features (commit, diff, checkout, revert) are exposed in the v1 UI. These operate at the database level, so changes across projects can get mixed — filters are implemented to reduce confusion, but proper isolation would require per-project Dolt branches. - -### Tests - -64 tests across 3 test files (server integration, schema validation, Claude service), all passing. Uses Vitest + Supertest. - -## Quick start (local) +## Quick start ```bash npm install -# Create .env with your API keys (at least one provider required) -cp .env.example .env +# Create .env with your Anthropic API key +echo "ANTHROPIC_API_KEY=sk-ant-..." > .env -# Start Dolt database (requires Docker): -npm run dolt -# OR without Docker: -dolt sql-server # then: dolt sql -> CREATE DATABASE brunch; - -# Optionally run OpenCode for multi-model support -opencode serve - -# Start dev server (frontend on :5173, API on :3001) +# Start dev server (frontend on :5173, API on :3000) npm run dev ``` Open http://localhost:5173. -## Docker Compose - -Starts both Dolt and the app in a single command: - -```bash -cp .env.example .env -# Edit .env and add your ANTHROPIC_API_KEY (or another provider key) - -docker compose up -d -``` - -Open http://localhost:3001. The container serves both frontend and API on a single port. - -## Docker (app only) - -If you have Dolt running separately: - -```bash -docker build -t brunch . - -docker run -d \ - --name brunch \ - -p 3001:3001 \ - -e ANTHROPIC_API_KEY=sk-ant-... \ - -e DOLT_HOST=host.docker.internal \ - brunch -``` - ## Environment variables | Variable | Required | Description | |---|---|---| -| `ANTHROPIC_API_KEY` | At least one provider key | Anthropic Claude API key | -| `OPENAI_API_KEY` | At least one provider key | OpenAI API key | -| `GOOGLE_GENERATIVE_AI_API_KEY` | At least one provider key | Google Gemini API key | -| `PORT` | No | Server port (default: `3001`) | -| `DOLT_HOST` | No | Dolt host (default: `localhost`) | -| `DOLT_PORT` | No | Dolt port (default: `3307` for local dev, `3306` inside Docker network) | -| `DOLT_USER` | No | Dolt user (default: `root`) | -| `DOLT_PASSWORD` | No | Dolt password (default: empty) | -| `DOLT_DATABASE` | No | Dolt database name (default: `brunch`) | -| `OPENCODE_URL` | No | OpenCode server URL (enables GPT-4o, Gemini models) | +| `ANTHROPIC_API_KEY` | Yes | Anthropic API key | +| `ANTHROPIC_MODEL` | No | Interviewer model (default: `claude-sonnet-4-20250514`) | +| `OBSERVER_MODEL` | No | Observer model (default: `claude-haiku-4-5-20251001`) | ## Scripts | Command | Description | |---|---| -| `npm run dev` | Start frontend (Vite :5173) + API (:3001) concurrently | +| `npm run dev` | Start frontend (Vite :5173) + API (:3000) with hot reload | | `npm run server` | Start API server only | -| `npm run build` | Build frontend for production (`/dist`) | -| `npm test` | Run Vitest test suite | -| `npm run dolt` | Start a Dolt Docker container on port 3307 | +| `npm run build` | Build frontend for production | +| `npm run test` | Run test suite (vitest) | +| `npm run verify` | Full gate: lint + format + test + build | +| `npm run fix` | Auto-fix lint + format issues | ## Architecture ``` -client/src/ -├── pages/ -│ ├── Home/ # v1 interface — hooks + components per feature -│ └── CreateSpec/ # v2 wizard — step-based screens -├── components/ # Shared (Header, LoadingIndicator) -└── assets/ - -server/ -├── server.js # Express app, middleware, route registration -├── db.js # Dolt connection pool, auto-migration -├── schemas.js # JSON Schema definitions for AI responses -├── models.js # Available model definitions -├── routes/ # One file per endpoint group -│ ├── stream.js # POST /api/stream (streaming queries) -│ ├── clarifying.js # POST /api/clarifyingquestions -│ ├── requirements.js # Requirements CRUD + generation -│ ├── assumptions.js # Assumptions CRUD -│ ├── sessions.js # Session lifecycle -│ ├── spec.js # Spec generation/storage -│ ├── specWizard.js # Wizard-mode endpoints -│ ├── versions.js # Dolt version control -│ └── history.js # LLM call log -├── services/ -│ ├── dispatch.js # Route to Claude or OpenCode by model -│ ├── claude.js # Anthropic Claude SDK (streaming, tools, structured output) -│ └── opencode.js # OpenCode SDK wrapper -├── middleware/ # Logging, validation, error handling -└── migrations/ # SQL schema migrations (auto-applied) +src/ +├── client/ +│ ├── routes/ +│ │ ├── InterviewWorkspace.tsx # Main interview UI +│ │ ├── ProjectList.tsx # Project dashboard +│ │ ├── ExportPreview.tsx # (placeholder) +│ │ └── ComponentDebug.tsx # Dev: component states +│ ├── components/ +│ │ ├── ai-elements/ # Chat UI components (AI Elements) +│ │ └── EntitySidebar.tsx # Decisions + assumptions sidebar +│ ├── router.tsx # TanStack Router (code-based) +│ └── main.tsx # React + QueryClient + Router +│ +├── server/ +│ ├── app.ts # Express routes + AI SDK stream composition +│ ├── core.ts # Turn preparation, project state, prompt extraction +│ ├── interview.ts # ToolLoopAgent interviewer + ask_question tool +│ ├── observer.ts # generateObject observer + entity persistence +│ ├── context.ts # Typed context builders (interviewer, observer) +│ ├── db.ts # SQLite via Drizzle + better-sqlite3 +│ ├── schema.ts # Drizzle schema (turns, options, decisions, assumptions, ...) +│ ├── parts.ts # Zod-validated parts serialization/deserialization +│ └── tools/ # Core filesystem tools (read, write, edit, bash, grep, find, ls) +│ +└── shared/ + ├── chat.ts # BrunchUIMessage types, Zod schemas, tool definitions + └── api-types.ts # API response types derived from server functions ``` ### Data flow -1. User input → Preact component → API call -2. Express route → validation middleware → service layer -3. Service dispatches to Claude SDK or OpenCode SDK (streaming NDJSON) -4. AI response parsed → database upsert (Dolt) -5. NDJSON stream → frontend hook → UI update +1. User input → `useChat` (AI SDK React) → `DefaultChatTransport` → POST `/api/projects/:id/chat` +2. Express validates incoming `BrunchUIMessage[]` via `validateUIMessages` +3. `prepareTurn()` creates a turn in the turn tree, builds interviewer context +4. `ToolLoopAgent` streams response → `toUIMessageStream()` → `pipeUIMessageStreamToResponse()` +5. On stream finish: observer runs (`generateObject`), entities persisted, `data-observer-result` part emitted in-band +6. Client `useChat` accumulates parts; `onData` invalidates entity query; `onFinish` refreshes project state ### Key patterns -- **Streaming**: All AI endpoints stream NDJSON chunks to the frontend -- **Tool use**: Claude Agent SDK with MCP tools for mutations (set goal, manage assumptions/requirements) -- **Hierarchical data**: Requirements are a self-referencing tree (`entry.parent_id`) -- **UUID stability**: Entities use UUIDs for identity across AI-generated upserts -- **Auto-migration**: `db.js` checks table/column existence on startup and applies missing migrations +- **Turn tree**: Conversations are branching trees, not flat logs. Each turn points to a parent. `active_turn_id` is HEAD. Active path resolved via recursive CTE. +- **Two-agent pattern**: Interviewer asks structured questions. Observer extracts decisions/assumptions after each turn. Different models, different prompts, independent testability. +- **Typed message contract**: `BrunchUIMessage` (AI SDK `UIMessage` with custom generics) spans server validation, persistence, streaming, and client hydration. +- **Parts-based persistence**: `assistant_parts` and `user_parts` JSON columns store the full UI state per turn. Scalar fields (`question`, `why`, `impact`) retained for queryability. +- **Zod everywhere**: Tool input/output schemas, data part schemas, parts deserialization, API payload validation. -## OpenCode (optional — multi-model support) +## Current state -By default, Brunch uses the Anthropic Claude API directly. To use additional models (GPT-4o, Gemini, etc.), run [OpenCode](https://opencode.ai) as an alternative backend. +**Working**: Scope-phase interview with structured questions, observer entity extraction, entity sidebar, conversation persistence and resume, project management. -1. Install: `npm i -g opencode` -2. Start: `opencode serve` -3. Set `OPENCODE_URL=http://localhost:4096` in `.env` -4. Start Brunch normally — the model dropdown will include additional models +**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). -OpenCode runs as a separate process with a REST+SSE API. The `opencode.json` config registers Brunch's assistant tools as an MCP server. Claude models always use the Anthropic API directly. +**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. -For Docker Compose: `export OPENCODE_URL=http://host.docker.internal:4096` before `docker compose up`. +## Tests -## Next steps +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. -### DX / Architecture - -- **Consolidate to one frontend**: Keep the v2 wizard as the primary UI. Repurpose v1 as an admin/debug dashboard (LLM logs, Dolt version control). This was started on the `branch` branch. -- **Fix Dolt project isolation**: Currently all projects share a single Dolt branch, so commits/reverts affect everything. Each project should get its own Dolt branch, merging only at strategic points. This requires tracking active branches and applying migrations per branch. -- **Proper migration runner**: The current approach in `db.js` checks for specific tables/columns and runs migrations conditionally. A numbered migration runner with a `schema_version` table would be more maintainable. -- **Consider SQLite compatibility**: Dolt is powerful but heavy for single-user local installs. Supporting SQLite as an alternative (dropped early on) would lower the barrier to entry. -- **Use AI SDK (Vercel) directly**: Replace the OpenCode dependency with `ai` (Vercel AI SDK) for multi-provider support without requiring a separate server process. OpenCode's `@opencode-ai/sdk` already depends on it. -- **Block-based data model**: The current schema (project → entries + assumptions) is rigid. A more flexible block-based model (like Notion) would better support evolving spec structures and richer content types. +```bash +npm test +``` -### User-facing improvements +## Project planning -- **Better prompts and workflow**: Clarifying questions are the most useful step. After that, the spec tends to reiterate what was already said. The prompt engineering and workflow sequencing need work to produce genuinely additive output at each stage. -- **Executable test generation**: Requirements currently generate test descriptions (plain text). The goal should be generating actual executable test cases that coding agents can run against implementations. -- **Richer requirement editing**: Inline editing of requirements is basic. Support for markdown content, attachments, and linking between requirements would make the spec more useful as a working document. -- **Export formats**: Generate specs in formats that integrate with existing tools (GitHub Issues, Linear, Markdown docs, YAML task definitions). -- **Multi-user support**: Currently single-user. Adding auth and collaborative editing would make it usable in team settings. -- **Feedback loop**: Let users rate AI-generated questions/assumptions/requirements so the system can improve over time. +- `memory/SPEC.md` — What and why (requirements, assumptions, decisions, invariants, verification) +- `memory/PLAN.md` — What's next (phases, slices, spikes, dependencies) +- `AGENTS.md` — Agent/AI coding instructions (symlinked as `CLAUDE.md`) diff --git a/docs/design/INTERVIEW_MODE_MODEL.md b/docs/design/INTERVIEW_MODE_MODEL.md new file mode 100644 index 00000000..567138f3 --- /dev/null +++ b/docs/design/INTERVIEW_MODE_MODEL.md @@ -0,0 +1,221 @@ +# Interview mode model + +_Date: 2026-04-07_ +_Status: design note backing the current SPEC patch_ + +## Purpose + +Record the revised runtime model surfaced during the flow talkthrough: + +- one interview is the throughline +- the interview moves through workflow modes (`scope`, `design`, `requirements`, `criteria`) +- entity capture is broader than `decision` + `assumption` +- entity capture is **capture-anytime, review-in-phase** +- `turn` remains the branching history spine +- readiness needs explicit artifacts rather than only turn-local booleans + +## Four-layer model + +1. **History spine** — `turn` tree plus `project.active_turn_id` +2. **Workflow mode** — phase/mode on turns plus explicit phase outcomes +3. **Knowledge layer** — durable knowledge items plus graph edges and provenance +4. **Readiness layer** — explicit review and closure state + +These layers are related, but should not be collapsed into each other. + +## Minimal expanded ontology + +### Durable knowledge kinds + +- **framing** — contextual truth / intent / problem context +- **constraint** — boundary on acceptable solution space +- **decision** — chosen fork / commitment +- **assumption** — belief that could prove false +- **requirement** — must-do capability +- **criterion** — verifiable success condition + +### Suggested subtype examples + +- `framing:user-need` +- `framing:problem-statement` +- `constraint:non-goal` +- `criterion:kpi` +- `criterion:benchmark` + +Start with `kind + subtype`, not table explosion. + +## Core interaction rule + +### Capture-anytime, review-in-phase + +Any mode may surface any knowledge kind. + +Each mode is responsible for **closing** the review state of the item families it owns: + +- **scope** owns framing / constraints sufficiency +- **design** owns decision / assumption coherence +- **requirements** owns requirement completeness and approval +- **criteria** owns verification coverage and approval + +## Schema direction + +### Turn remains central, but becomes more generic + +Recommended direction: + +- keep `turn` as the versioned, branching checkpoint +- stop forcing all turn semantics into scalar `question` / `answer` columns +- store mode-shaped interaction payloads as typed JSON validated by Zod + +### Suggested storage model + +#### `project` +- `id` +- `name` +- `active_turn_id` +- timestamps + +#### `turn` +- `id` +- `project_id` +- `parent_turn_id` +- `phase` +- `kind` (`interaction | review | system | edit`) +- `prompt_payload` +- `response_payload` +- `user_parts` +- `assistant_parts` +- timestamps + +#### `knowledge_item` +- `id` +- `project_id` +- `kind` +- `subtype` +- `content` +- `rationale` +- timestamps + +#### `turn_knowledge_item` +- `turn_id` +- `item_id` +- `relation` (`captured | confirmed | edited | invalidated | reviewed`) + +#### `knowledge_edge` +- `from_item_id` +- `to_item_id` +- `relation` (`depends_on | derived_from | constrains | verifies | refines`) + +#### `phase_outcome` +- `id` +- `project_id` +- `phase` +- `status` (`proposed | confirmed | superseded`) +- `source_turn_id` +- `confirmed_turn_id` +- `invalidated_by_turn_id` +- `summary` +- timestamps + +#### `knowledge_review` +- `id` +- `item_id` +- `phase` +- `status` (`pending | approved | edited | rejected | stale`) +- `source_turn_id` +- `superseded_by_turn_id` +- timestamps + +## Turn response model + +### Minimal response forms + +1. **single-select with rationale** +2. **multi-select with rationale** +3. **custom answer / none-of-the-above** +4. **review response** (`approve | edit | reject | add-missing`) + +### Design principle + +Keep the interviewer’s structured guidance, options, recommendations, and “why this matters” affordance. + +Remove the assumption that every answer is a single categorical pick. + +## Mode behavior summary + +### Scope mode — framing + +**Primary job** +- establish a sufficient framing bundle + +**Observer bias** +- prefer `framing`, `constraint`, early `requirement` +- do not force framing facts into `assumption` + +**Closure** +- enough clarity to enter design without asking identity questions again + +### Design mode — commitment / exploration + +**Primary job** +- resolve major design forks and expose supporting assumptions + +**Observer bias** +- prefer `decision`, `assumption`, plus newly surfaced `constraint`, `requirement`, and framing corrections + +**Closure** +- major design forks are resolved enough to synthesize a stable requirement set + +### Requirements-review mode — audit / completeness + +**Primary job** +- normalize, complete, and confirm the requirement set implied by the whole knowledge layer + +**Observer bias** +- prefer new or refined `requirement` plus derivation edges + +**Closure** +- requirement set is complete enough for verification work and each in-scope requirement is approved, edited into approval, or explicitly rejected + +### Criteria-review mode — verification + +**Primary job** +- turn approved requirements into verifiable success conditions + +**Observer bias** +- prefer new or refined `criterion` plus `verifies` edges + +**Closure** +- every in-scope approved requirement has sufficient verification coverage + +## Revisit and invalidation + +The turn tree still owns revisitation. + +Changing an upstream turn invalidates downstream readiness from that frontier: + +- changing framing invalidates design + later review outcomes +- changing design invalidates downstream requirement / criteria review outcomes +- changing requirements invalidates downstream criteria review outcomes +- changing criteria invalidates criteria review completeness, and may escalate upstream if a requirement defect is exposed + +This suggests phase outcomes and review records should be invalidated explicitly, not inferred only from `turn.is_resolution` or `reviewed_at` timestamps. + +## Export readiness + +A project is export-ready only when: + +- each mode has a confirmed, non-invalidated active-path phase outcome +- all in-scope requirements are review-complete +- all in-scope criteria are review-complete +- no unresolved upstream staleness remains + +## Why this model + +This is the smallest model found so far that: + +- preserves the elegant turn-tree revisit story +- matches what the interview actually produces in practice +- supports richer later-phase synthesis and review behavior +- avoids table explosion by using `kind + subtype` +- keeps future UI and observer evolution possible without rewriting the history model diff --git a/docs/design/ln-skills-comparative-analysis.md b/docs/design/ln-skills-comparative-analysis.md new file mode 100644 index 00000000..ceca4121 --- /dev/null +++ b/docs/design/ln-skills-comparative-analysis.md @@ -0,0 +1,300 @@ +# `dev-*` vs `ln-*` Comparative Analysis + +Date: 2026-04-06 + +> Note: this document captures the comparative assessment at the time it was written. Several `ln-*` WIP gaps named below were subsequently tightened in local skill/template edits on 2026-04-06, but the analysis remains useful as a snapshot of what needed alignment. + +## Purpose + +This note captures a comparative assessment of the `dev-*` skill family in `dot-agents` against the `ln-*` rewrite in `brunch`. + +It has two goals: + +1. Preserve the current assessment for ongoing `ln-*` development. +2. Define a **targeted back-port set**: the parts of `ln-*` that should likely fold back into `dev-*` without importing the full heavier protocol stack. + +One important exception: `dev-talkthrough` is a later addition that exists only in `dev-*`. It should be preserved as a distinct skill rather than treated as a missing `ln-*` equivalent. + +## High-Level Verdict + +It looks like time to consolidate, but not by replacing `dev-*` wholesale with `ln-*`. + +The `ln-*` rewrite has real methodological improvements, especially around: + +- updating existing planning artifacts instead of always writing from scratch +- treating uncertainty as a first-class planning input +- separating verification strategy into an explicit design activity +- tightening cross-document traceability + +But `ln-*` is still visibly WIP. Some of its strongest ideas depend on a heavier document contract that does not exist in `dev-*` yet, and some files still carry local workflow assumptions or internal inconsistencies. + +Recommendation: treat `ln-*` as a design branch and **selectively merge its stronger ideas back into `dev-*`**, while preserving the lighter-weight character of the `dev-*` family. + +## What `ln-*` Improves + +### 1. Update-aware spec and plan workflows + +`ln-spec` and `ln-plan` are much better than their `dev-*` counterparts at handling living documents. + +- `ln-spec` supports a clear **patch vs full pass** distinction. +- `ln-spec` treats an existing `memory/SPEC.md` as prior state to evolve, not a blank slate. +- `ln-plan` supports targeted edits and re-ordering instead of assuming a full rewrite. +- `ln-plan` explicitly retires completed work and reassesses remaining work before planning forward. + +This is a meaningful improvement over the simpler `dev-spec` / `dev-plan` model. + +### 2. Uncertainty-aware planning + +`ln-plan` introduces two strong ideas that should probably survive consolidation: + +- **Epistemic horizon**: do not plan deeper than current confidence supports. +- **Spike economics**: evaluate assumptions by fan-out, falsification cost, and decision unlock value. + +This makes planning sharper than a generic slice list. It turns planning into sequencing under uncertainty rather than mere decomposition. + +### 3. Better elicitation in `ln-grill` + +`ln-grill` improves the `dev-grill` posture in two useful ways: + +- It explicitly names anti-patterns when a design is drifting toward one. +- It sharpens lexicon formation during elicitation instead of waiting until spec-writing. + +Those are good judgment upgrades and do not require a heavier protocol to be useful. + +### 4. Stronger scope/build contract + +`ln-build` is clearer that the canonical path is `scope -> build`, and that a raw behavior description is only acceptable when scoping would be pure ceremony. + +That is a good tightening of the execution loop. + +### 5. Better sync discipline + +`ln-sync` is materially more mature than `dev-sync`. + +Its strongest improvement is the **pruning model**: + +- assumptions and decisions are not meant to accumulate forever +- tracked items can become confusion surfaces once they are embedded, moot, or superseded +- stable IDs should survive pruning; surviving records should not be renumbered + +This is valuable, though it depends on a more structured document format than current `dev-*` uses. + +### 6. Verification as first-class design work + +`ln-oracles` is the largest genuine expansion in the rewrite. + +It contributes three important ideas: + +- verification strategy deserves its own skill rather than being buried in generic spec prose +- observability / reproducibility / controllability are useful diagnostic dimensions before selecting test strategy +- oracle families and loop tiers are a better framing than only saying "unit / integration / e2e" + +This is the strongest conceptual addition in `ln-*`. + +## Where `ln-*` Is Still WIP + +### 1. The family router does not know the whole family + +`ln-consult` does not route to `ln-oracles`, even though `ln-oracles` is clearly treated elsewhere as part of the core method. + +That means the family's entrypoint does not fully model the family it is supposed to triage. + +### 2. Handoff does not cover the full system + +`ln-handoff` is still modeled around only part of the lifecycle. + +Its flow sketch covers: + +`grill -> spec -> plan -> scope -> [spike] -> build -> review -> [sync]` + +That omits: + +- `ln-design` +- `ln-refactor` +- `ln-oracles` + +Its volatile-artifact checklist likewise omits these skill outputs. + +### 3. Scope knows about oracles, but routing does not fully reflect that + +`ln-scope` can tell the agent to run `ln-oracles` first if no oracle strategy exists, but it does not include `ln-oracles` in its routing options. + +That is a real consistency gap. + +### 4. Some command references are too local + +`ln-spec` uses slash-command references like `/ln-grill` and `/ln-design`. + +That feels tool-environment-specific rather than skill-system-generic, and would likely want normalization before broader adoption. + +### 5. PRD language still lingers + +`ln-grill` still says: + +> Understanding is sufficient for a PRD + +This is a carry-over relic, not aligned with the current `SPEC.md` / `PLAN.md` vocabulary. + +### 6. The issue/branch protocol is embedded in live templates + +This is the biggest reason not to import `ln-*` wholesale. + +The `ln-plan` template still includes live fields for: + +- `ISSUE-ID` +- `Branch` + +Those are not just provenance notes; they shape output. That makes the current `ln-*` document model partly brunch-local. + +### 7. Internal contract drift is now a bigger problem than wording drift + +The strongest `ln-*` ideas now depend on a richer document schema than the actual templates reliably provide. + +Examples: + +- `ln-scope`, `ln-plan`, and `ln-build` assume `PLAN.md` can carry parallelism, verification approach, and invariant-establishment bookkeeping, but the template does not fully support those fields. +- `SPEC.md` models assumptions primarily by confidence, while `ln-build`, `ln-spike`, and `ln-sync` talk as though assumptions have validation states (`validated` / `invalidated`) as first-class values. +- Several `ln-*` files still carry visible fork residue (`# Dev ...`, `roadmap`, `PRD`) that weakens the sense of a coherent local method. +- `ln-handoff` says to write at the workspace root while its template resume prompt tells the next thread to read from the project root. + +This means the next round of `ln-*` work should prioritize **schema and lifecycle alignment** before deeper methodological expansion. + +## The Core Architectural Difference + +The two families are not just two phrasings of the same workflow. + +### `dev-*` is a lighter conversational methodology + +`dev-*` relies on helpful artifacts, but the documents are comparatively loose: + +- `dev-plan` creates a simple roadmap +- `dev-spec` creates a broad spec +- `dev-build` does not require systematic document maintenance after implementation + +This makes it easier to use and easier to port across repos. + +### `ln-*` is a governed document system + +`ln-*` assumes stronger structure in `SPEC.md` and `PLAN.md`: + +- numbered requirements +- stable assumption IDs +- decision supersession chains +- invariant tracking +- explicit coverage tables +- per-slice traceability +- ownership boundaries between skills + +That structure is powerful when the whole family is aligned around it, but expensive when only half the system participates. + +## Targeted Back-Port Set + +These are the `ln-*` ideas that look worth folding back into `dev-*`. + +### Safe to port soon + +1. Patch/update mode for `dev-spec` +2. Patch/update mode for `dev-plan` +3. Epistemic horizon and spike economics in planning +4. Anti-pattern naming and lexicon-tightening in `dev-grill` +5. Stronger "scope card first" language in `dev-build` +6. A dedicated `dev-oracles` skill, introduced as optional hardening rather than mandatory ceremony + +### Worth porting later, only if the document model grows + +1. Pruning with stable IDs in `dev-sync` +2. Invariant tracking +3. Rich cross-reference integrity checks +4. Post-build traceability bookkeeping into `SPEC.md` / `PLAN.md` + +These depend on a more structured artifact format. They should not be imported before the destination document model can support them cleanly. + +## What Should Not Be Ported Back + +### 1. Issue-tracker and branch metadata in planning artifacts + +Do not port the live `ISSUE-ID` and `Branch` fields from `ln-plan` templates into `dev-*`. + +Those are brunch workflow bindings, not generally reusable methodology. + +### 2. Mandatory bookkeeping before the schema exists + +Do not port the mandatory post-build or post-spike bookkeeping from `ln-build` / `ln-spike` until `dev-*` has a document model capable of supporting it coherently. + +Otherwise the process becomes heavier without actually becoming clearer. + +### 3. Renaming `ROADMAP.md` to `PLAN.md` + +Do not treat this as a superficial rename. + +In `ln-*`, `PLAN.md` is tied to a deeper shift in structure and traceability. The name only makes sense in the context of that broader contract. + +### 4. Dropping `dev-talkthrough` + +`dev-talkthrough` is a later addition in `dev-*`, not a failed or missing part of `ln-*`. + +It should be preserved as a distinct outside-in explanatory skill rather than forced into the main lifecycle. + +## Recommended Consolidation Strategy + +### Near term + +Evolve `dev-*`, do not replace it. + +Recommended first ports: + +1. `ln-grill` improvements into `dev-grill` +2. `ln-spec` patch/update behavior into `dev-spec` +3. `ln-plan` uncertainty-aware planning into `dev-plan` +4. `ln-build` scope-card-first behavior into `dev-build` + +### Next layer + +Introduce a lighter-weight `dev-oracles`: + +- verification strategy as explicit work +- loop tiers and oracle families as design vocabulary +- no requirement that every slice carry the full `ln-*` oracle protocol +- no dependency on branch/issue metadata + +### Only after that + +If `dev-*` later adopts more structured artifacts, then revisit: + +- invariant tracking +- stable IDs in assumptions / decisions +- pruning rules in sync +- cross-reference integrity checks +- richer completion bookkeeping + +## Implications for Ongoing `ln-*` Development + +If `ln-*` continues as a local brunch methodology, it should choose one of two directions explicitly: + +### Option A: finish the governed-document model + +If the intent is to keep the heavier protocol, then `ln-*` should be tightened so the whole family actually participates in it: + +- add `ln-oracles` to `ln-consult` +- add `ln-oracles`, `ln-design`, and `ln-refactor` to `ln-handoff` +- fix `ln-scope` routing around oracle design +- remove PRD wording +- normalize tool-local command references + +### Option B: trim back to the portable core + +If the intent is to align closer to `dev-*`, then the best path is probably: + +- keep the update-aware planning/spec behavior +- keep the uncertainty and verification thinking +- remove workflow-specific artifact fields from templates +- reduce mandatory bookkeeping that depends on the full traceability regime + +## Working Recommendation + +For consolidation work, the best default posture is: + +1. Preserve `dev-talkthrough` as-is. +2. Port the high-value, low-ceremony `ln-*` improvements back into `dev-*`. +3. Do **not** import the full governed protocol unless `dev-*` intentionally grows into that stronger document system. +4. Treat `ln-oracles` as the most promising new skill to adapt, but adapt it downward into a lighter-weight form first. diff --git a/docs/design/ln-skills-review-after-alignment.md b/docs/design/ln-skills-review-after-alignment.md new file mode 100644 index 00000000..e8788e07 --- /dev/null +++ b/docs/design/ln-skills-review-after-alignment.md @@ -0,0 +1,267 @@ +# `ln-*` Skills Review After Alignment + +Date: 2026-04-06 + +## Purpose + +This note captures a post-alignment review of the local `ln-*` skill family after the recent cleanup pass. + +It is meant to preserve: + +1. the current assessment of the family as a working system +2. the main risks that still deserve observation in real use +3. the recommended next feedback loop: use the skills, then review behavior rather than continuing speculative redesign + +## Executive Summary + +The `ln-*` family now appears to be in good working shape. + +The biggest earlier problems were: + +- family members not modeling the full family +- template/schema mismatches +- fork residue from `dev-*` +- project-local workflow assumptions embedded too directly into the generalized planning method + +Those are now mostly resolved. + +Current verdict: + +- no major internal contradictions remain +- the family has a clearer identity distinct from `dev-*` +- the document model now more closely matches what the skills ask the agent to do +- the remaining risks are mostly about calibration and ergonomics, not correctness + +## What Now Feels Strong + +### 1. The family understands its own lifecycle + +The family router and downstream skills now model the larger method more coherently: + +- `ln-consult` includes `ln-oracles` +- `ln-scope` can route to `ln-oracles` +- `ln-handoff` includes `ln-design`, `ln-oracles`, and `ln-refactor` +- the canonical flow is easier to read as one system rather than a loose collection of prompts + +This gives the skill family a stronger methodological spine. + +### 2. The skill instructions and document schemas now mostly agree + +This is the most important practical improvement. + +Notable gains: + +- `PLAN.md` now has room for parallelism, verification approach, candidate invariant goals, invariants established, and optional execution tracking +- `SPEC.md` now distinguishes assumption confidence from validation status +- `ln-build`, `ln-spike`, and `ln-sync` now operate against that same assumptions schema +- bookkeeping steps now more closely match actual fields in the templates + +The system is much less likely to ask an agent to write into a structure that does not exist. + +### 3. Local workflow protocol is no longer the core planning model + +The `ln-plan` skill now treats issue/ticket mapping and branch naming as local project protocol rather than universal method. + +That is the right abstraction boundary: + +- projects may have their own execution workflow +- `ln-plan` should respect those protocols +- but slice identity should remain conceptual, not tied to one tracker or branch scheme + +This makes the family more reusable without discarding project-specific rigor. + +### 4. `ln-oracles` now feels integrated instead of appended + +`ln-oracles` was previously the strongest conceptual addition but the least integrated operationally. + +It now has: + +- better routing +- better relationship to `ln-scope` +- a clearer optional-vs-required posture +- a sharper boundary with `ln-spec` + +That makes it feel like a real member of the family, not an isolated extra. + +## Remaining Risks To Watch + +These are not current defects so much as operational watch items. + +### 1. Method heaviness + +This is now a coherent governed-document system, but it is still heavier than `dev-*`. + +The risk is not contradiction; it is over-ritualization. + +Watch for cases where agents: + +- over-scope trivial work +- over-update planning artifacts for small changes +- invoke too much bookkeeping relative to the size of the task +- route through `ln-oracles` when direct inner-loop checks would have sufficed + +This has already been mitigated somewhat through: + +- patch/update modes +- trivial/purely structural exceptions +- optional oracle design for simple slices + +But the family should still be observed for ceremony creep. + +### 2. `PLAN.md` may become over-specified in practice + +The richer plan template is now much better aligned with the method, but it is also denser. + +A slice can now carry: + +- requirements +- assumptions +- candidate invariant goals +- invariants to respect +- acceptance +- verification approach +- invariants established +- optional execution tracking + +That is powerful when these fields carry real information. + +It becomes noise if agents fill them mechanically. + +Watch for signs of placeholder behavior such as: + +- generic verification text that says little +- invariant fields populated before any meaningful understanding exists +- execution tracking added before it is useful +- every slice looking equally elaborate regardless of risk + +If this appears, the next refinement should likely be stronger guidance about when fields may be left intentionally minimal. + +### 3. Refactor execution remains slightly less explicit than feature-slice execution + +`ln-build` can now accept a commit-sized step from `memory/REFACTOR.md`, which resolves the direct contract conflict with `ln-refactor`. + +Still, the build skill is clearest when given: + +- target behavior +- acceptance criteria +- verification approach + +A refactor step may not always present those as cleanly as a scope card. + +Watch for whether agents executing refactor steps: + +- infer too much +- widen the step beyond one safe commit +- produce weak or ambiguous verification + +If this becomes a recurring issue, the next iteration might introduce a more explicit “refactor execution card” shape inside `ln-refactor`. + +### 4. `ln-design` still assumes a subagent-rich environment + +`ln-design` still instructs the agent to spawn 3+ sub-agents with divergent constraints. + +That is acceptable locally, but it remains one of the few places where the family still strongly reflects a particular execution environment. + +Watch for: + +- failure when parallel subagenting is unavailable or awkward +- overuse of subagent generation when a lighter design pass would suffice +- low-quality variation across the generated alternatives + +This is not a correctness issue, just a portability and execution-quality watch item. + +### 5. `ln-sync` remains the most judgment-sensitive skill + +`ln-sync` is conceptually strong but depends heavily on judgment: + +- what counts as embedded vs moot vs superseded +- when to prune vs preserve +- how aggressively to repair cross-references + +Watch for: + +- helpful simplification vs accidental truth loss +- over-pruning of useful rationale +- under-pruning that leaves stale context alive +- repair of refs that is formally correct but semantically weak + +This may remain a skill best suited to stronger agents or more deliberate sessions. + +### 6. `ln-handoff` is doctrinally strong but execution-dependent + +The handoff skill now reflects the family more faithfully, including design, oracle, and refactor state. + +The risk here is not method design but discipline: + +- can the agent actually capture all volatile state with fidelity? +- does it preserve evidence, not just conclusions? +- does it treat persisted artifacts and chat-only artifacts differently enough? + +Watch for handoffs that are structurally complete but operationally thin. + +## Signals To Watch In Real Use + +The best next feedback will come from actual use of the family, not another abstract redesign pass. + +Key questions: + +### Routing quality + +- Does `ln-consult` suggest the right next skill? +- Does `ln-scope` correctly distinguish “build now” from “design oracles first”? +- Does `ln-spike` route invalidations toward `ln-spec` vs `ln-plan` sensibly? + +### Planning quality + +- Does `ln-plan` produce plans that clarify sequencing under uncertainty? +- Do slices remain thin and demoable? +- Do candidate invariant goals and verification approach fields improve planning rather than bloat it? + +### Verification quality + +- Do `ln-oracles` outputs materially improve slice scoping and test design? +- Do recent slices actually implement the promised verification approaches? +- Do blind spots get surfaced honestly? + +### Build quality + +- Does `ln-build` keep slices small? +- Does bookkeeping after builds feel worth the overhead? +- Are invariants and coverage updates informative rather than ceremonial? + +### Documentation quality + +- Does `ln-sync` improve clarity? +- Are assumptions, decisions, and invariants evolving cleanly? +- Do `SPEC.md` and `PLAN.md` become more navigable over time, not less? + +### Handoff quality + +- Can a new thread resume from `HANDOFF.md` without clarifying questions? +- Are volatile artifacts preserved with enough detail to prevent re-investigation? +- Is review debt and verification state visible enough to survive thread boundaries? + +## Recommended Next Step + +Do not continue speculative redesign immediately. + +Instead: + +1. use the `ln-*` skills on real slices +2. observe where they produce clarity vs overhead +3. collect examples of good and bad outputs +4. review again after some real usage + +The family now seems coherent enough that empirical behavior is the highest-value source of further improvement. + +## Bottom Line + +The `ln-*` family now feels like a real governed planning-and-execution system rather than a partially renamed fork. + +What remains to evaluate is less about fixing contradictions and more about watching how the method behaves under real load: + +- Does it stay sharp? +- Does it stay proportional? +- Does the extra structure pay for itself? + +Those are now the right questions. diff --git a/docs/praxis/dev-server-logs.md b/docs/praxis/dev-server-logs.md new file mode 100644 index 00000000..4b885ded --- /dev/null +++ b/docs/praxis/dev-server-logs.md @@ -0,0 +1,61 @@ +# Dev Server & Debugging Tools + +Runtime tooling for observing the running application — logs, database inspection, and process management. + +## How it works + +The `npm run dev` script wraps two services with `agent-tail run`: + +- **`vite`** — frontend dev server → `tmp/logs/latest/vite.log` +- **`api`** — backend server → `tmp/logs/latest/api.log` + +The Vite plugin (`agentTail()` in `vite.config.ts`) captures browser `console.*` calls → `tmp/logs/latest/browser.log`. + +All output is also interleaved in `tmp/logs/latest/combined.log`. + +## Important + +The dev server **must** be started via `npm run dev` for server-side logs to be captured. If Vite is started directly (e.g. bare `vite`), only `browser.log` will exist — `vite.log`, `api.log`, and `combined.log` will be missing. + +## Orphan process cleanup + +The `dev` script kills any orphaned processes on ports 5173 (Vite) and 3000 (API) before starting. This prevents `EADDRINUSE` errors when a previous dev session was killed without clean shutdown (e.g. agent timeout, force-quit, crashed terminal). + +If you hit port conflicts outside `npm run dev`, kill orphans manually: + +```bash +lsof -ti:5173 | xargs kill -9 2>/dev/null +lsof -ti:3000 | xargs kill -9 2>/dev/null +``` + +## Reading logs + +Use `Read` or `Grep` against `tmp/logs/latest/`: + +```bash +# Check for server errors +grep -i error tmp/logs/latest/vite.log +grep -i error tmp/logs/latest/api.log + +# Browser console output +cat tmp/logs/latest/browser.log + +# Everything interleaved +cat tmp/logs/latest/combined.log +``` + +## Session history + +Each `npm run dev` invocation creates a timestamped session directory under `tmp/logs/`. The `latest` symlink always points to the most recent session. Older sessions remain available for comparison. + +## Drizzle Studio (database inspector) + +Browse and edit the SQLite database visually: + +```bash +npm run studio +``` + +Opens `https://local.drizzle.studio` in the browser. Reads from `brunch.db` (or `$BRUNCH_DB`) — the same file the API server writes to. Config lives in `drizzle.config.ts`. + +Note: tests use in-memory databases, so test data won't appear in Studio. Only data from actual dev server sessions (`npm run dev`) is visible. diff --git a/memory/PLAN.md b/memory/PLAN.md index 2f53928e..eabe6068 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -18,192 +18,159 @@ ### Slices -1. **Walking skeleton: SDK → SSE → React** `FE-534` — Prove the integration seam: the highest-uncertainty slice, retires the most risk. `done` - - Requirements: → SPEC.md §Requirements #1, #4 - - Assumptions: → SPEC.md §Assumptions A1, A2, A8, A10 - - Invariants established: → SPEC.md §Invariants I1, I2, I3, I4 - - Acceptance: `npm run dev` opens browser, type a message, see streamed response with visible thinking and text. `useChat` manages all state - - Branch: `ln/fe-534-walking-skeleton` - -2. **SQLite foundation + project persistence** `FE-535` — Replace Dolt with `better-sqlite3`. Basic persistence with project + message tables. Conversation history replay. `done` - - Requirements: → SPEC.md §Requirements #14 - - Assumptions: → SPEC.md §Assumptions A5 (validated), A11 (validated), A12 (validated) - - Invariants established: → SPEC.md §Invariants I5, I6 - - Invariants respected: → SPEC.md §Invariants I1, I2, I3 - - Acceptance: create project, send message, refresh page, see history, continue conversation - - Branch: `ln/fe-535-sqlite-persistence` - -## Phase 2: Architecture - - +1. **Walking skeleton: SDK → SSE → React** `FE-534` `done` — I1, I2, I3, I4 +2. **SQLite foundation + project persistence** `FE-535` `done` — I5, I6 -### Slices +## Phase 2: Architecture `done` -3. **Turn tree schema + API** `FE-544` — Migrate from message table to the full schema.dbml model (turn, option, decision, assumption, requirement, criterion + all join tables). Update API: POST /api/chat creates turns, GET /api/projects/current returns turns on the active path. Project gets `active_turn_id`. Tests verify turn tree CRUD and active path resolution. `done` - - Requirements: → SPEC.md §Requirements #14 - - Assumptions: → SPEC.md §Assumptions A6 - - Invariants established: → SPEC.md §Invariants I6 (updated), I9, I10 - - Invariants respected: → SPEC.md §Invariants I1, I2, I3 - - Acceptance: create project, create turns with parent chain, resolve active path, close and reopen with state intact - - Branch: `ln/fe-544-turn-tree-schema` - -3c. **Drizzle ORM + core extraction** `FE-552` — Migrate raw DDL to Drizzle schema (`drizzle/schema.ts`) with migration runner. Extract interview orchestration from `app.ts` into `core.ts` — `conductTurn()` returns `AsyncIterable`. Express handler becomes a thin adapter translating DomainEvents to SSE. `done` - - Requirements: → SPEC.md §Requirements #14 - - Assumptions: → SPEC.md §Assumptions A18 (validated), A19 (validated) - - Decisions: → SPEC.md §Decisions D18, D19 - - Invariants established: → SPEC.md §Invariants I11, I12, I13 - - Invariants respected: → SPEC.md §Invariants I1, I2, I3, I5, I6, I9, I10 - - Acceptance: 51 tests pass (39 existing + 12 new core tests); Drizzle migrate() auto-applies at startup; conductTurn() yields DomainEvents consumed by Express adapter via createDomainAdapter() - - Branch: `ln/fe-552-drizzle-core-extraction` - -3d. **Multi-project routing** `FE-553` — TanStack Router with three routes: project list (`/`), interview workspace (`/project/:id`), export preview placeholder (`/project/:id/export`). Route loaders replace `useEffect` hydration. Server API project-scoped (`/api/projects`, `/api/projects/:id`, `/api/projects/:id/chat`). `done` - - Requirements: → SPEC.md §Requirements #1, #15 - - Decisions: → SPEC.md §Decisions D9 (updated) - - Invariants established: → SPEC.md §Invariants I14, I15 - - Invariants respected: → SPEC.md §Invariants I1, I2, I3, I6, I9, I10 - - Acceptance: 72 tests pass (11 new: 6 db, 5 app); project-scoped API routes; TanStack Router with code-based routing; route loaders fetch data; DefaultChatTransport for project-scoped chat endpoint - - Branch: `ln/fe-553-multi-project-routing` - - Ref: → docs/design/BREADBOARD.md §Places, §Wiring - -## Phase 3: Interview Engine - - - -### Spikes - -1. **Observer extraction fidelity** `FE-557` — Can the LLM reliably extract decisions, assumptions, and dependency edges from a single turn's Q&A? Test with realistic fixture turns across different question types (scope, design, constraints). Measure extraction consistency across runs. `done` - - Assumptions: → SPEC.md §Assumptions A14, A3 - - Time box: 2 hours - - Success: ≥80% of expected entities captured with correct dependency edges across 5+ fixture turns - - **Verification approach**: differential oracle — fixture turns (input) → observer extraction (output) → compare against hand-labeled golden master. Spike must produce ≥5 reusable fixtures with expected entities as proof artifact. → SPEC.md §Oracle Strategy (middle loop), §Observer History Projection +3. **Turn tree schema + API** `FE-544` `done` — I6, I9, I10 +3c. **Drizzle ORM + core extraction** `FE-552` `done` — I11, I12, I13 +3d. **Multi-project routing** `FE-553` `done` — I14, I15 -### Slices +## Phase 3: Interview Engine `done` -3b. **Rich chat UI: tool calls + reasoning rendering** `FE-541` — Extend SSE adapter and core to emit tool-call lifecycle events for SDK `tool_use` content blocks. Part-type rendering for tool calls (with state indicator) and reasoning (collapsible block). AI Elements deferred — hand-built rendering sufficient for now. `done` - - Requirements: → SPEC.md §Requirements #4 - - Assumptions: → SPEC.md §Assumptions A16 (partially validated — SSE + client work, browser outer-loop pending), A17 (not yet tested — AI Elements not installed) - - Invariants established: → SPEC.md §Invariants I7 - - Invariants respected: → SPEC.md §Invariants I1, I2, I3 - - Acceptance: 61 tests pass (10 new: 6 SSE adapter, 3 core, 1 app integration); tool-call-streaming-start/delta/tool-call SSE events emitted for SDK tool_use blocks; client renders dynamic-tool parts with state labels - - Branch: `ln/fe-541-rich-chat-ui` - -4. **Structured interview: scope phase (server)** `FE-554` — Replace flat chat with structured turns. Implement the scope phase as an agent skill — the agent generates a question with options, grounding ("why this matters"), and impact signal via `ask_question` MCP tool. Turn persists with phase provenance (question, why, impact, options). `done` - - Requirements: → SPEC.md §Requirements #2, #3 - - Assumptions: → SPEC.md §Assumptions A7, A13 (validated) - - Invariants established: → SPEC.md §Invariants I16 - - Invariants respected: → SPEC.md §Invariants I1, I2, I3, I5, I6, I12, I13 - - Acceptance: 90 tests pass (16 new interview tests, 2 new app integration); `ask_question` MCP tool validates agent output via Zod schema; per-turn MCP server created via closure over db + turnId; `getSystemPrompt(phase)` returns phase-specific prompt; structured turn fields (question, why, impact, options) persist correctly - - Branch: `ln/fe-554-structured-interview` - - **Verification approach**: inner — schema validation on agent tool output (Zod parse, establishes I16); unit tests for phase-tagged turn persistence. Middle — round-trip: structured turn → persist → active path query → verify phase provenance intact. Outer — manual interview walkthrough, assess question quality. → SPEC.md §Oracle Strategy, §Acknowledged Blind Spots (interview quality) - -4a. **Parts-based persistence + context builders** `FE-555` — Schema migration: add `user_parts` and `assistant_parts` JSON columns to turn table. Server-side: assemble final assistant `parts[]` from DomainEvents on stream finish, persist alongside scalars. Define `BrunchUIMessage` type with custom Data Parts (`data-option-selection`, `data-confirmation`). Extract `formatHistory()` into typed context builders (`buildInterviewerContext`, `buildObserverContext`). No backward-compatible fallback — DB can be re-initialized if needed. `done` - - Requirements: → SPEC.md §Requirements #4, #14 - - Assumptions: → SPEC.md §Assumptions A22, A23 - - Decisions: → SPEC.md §Decisions D23, D24, D25 - - Invariants established: → SPEC.md §Invariants I17, I18, I19 - - Invariants respected: → SPEC.md §Invariants I1, I5, I6, I11, I12, I13, I16 - - Acceptance: schema migration adds parts columns; assistant parts persisted on stream finish (reasoning, tool-call states, text); Data Part schemas validated via Zod on write/read (I17); parts round-trip: DomainEvents → assemble → persist → load → hydrate matches original (I18); `buildInterviewerContext()` produces equivalent output to current `formatHistory()` (I19); observer context builder produces extraction-optimized projection - - Branch: `ln/fe-555-parts-persistence` - - **Verification approach**: inner — round-trip oracle for parts fidelity (I18); Zod schema validation on Data Parts (I17); unit tests for context builder output shape and equivalence (I19). → SPEC.md §Oracle Strategy (inner: fast unit tests — parts). Middle — integration: full `conductTurn()` → parts persisted → reload → hydration matches live state. Outer — manual resume test via `/cli-cdp` (reasoning + tool states visible on refresh). → SPEC.md §Acknowledged Blind Spots (parts/scalar consistency). - -4b. **Structured interview: client UI** `FE-556` — Turn card rendering (question + options + grounding + impact badge). Option selection UI using `data-option-selection` Data Part (persist `is_selected` + structured answer). Enriched API: turns with options + validated parts deserialization. Hydration from `assistant_parts`. Outer-loop visual verification via `/cli-cdp`. Also addresses review findings: validated deserialization (review #1) and DB lifecycle parts round-trip test (review #2). `done` - - Requirements: → SPEC.md §Requirements #2, #3 - - Assumptions: → SPEC.md §Assumptions A22, A23 - - Decisions: → SPEC.md §Decisions D23, D24 - - Invariants established: → SPEC.md §Invariants I17 (strengthened), I18 (strengthened) - - Invariants respected: → SPEC.md §Invariants I1, I16 - - Acceptance: enriched API returns turns with options + validated parts; turn card rendering; option selection persists as data-option-selection + is_selected; hydration from assistant_parts; refresh preserves state; outer-loop visual verification via `/cli-cdp` - - Branch: `ln/fe-556-interview-client-ui` - - **Verification approach**: inner — validated deserialization rejects malformed JSON (I17 strengthened); DB lifecycle round-trip covers parts (I18 strengthened); unit tests for select endpoint. Outer — manual interview walkthrough via `/cli-cdp`. → SPEC.md §Acknowledged Blind Spots (interview quality) - -4c. **UI foundation: shadcn/ui + Tailwind 4 + AI Elements** `FE-558` — Infrastructure realignment before slice 5. Install Tailwind 4 + `@tailwindcss/vite`, run `shadcn init`, install AI Elements core chat components (conversation, message, reasoning, tool, prompt-input, shimmer). Update `ai` + `@ai-sdk/react` to latest. Migrate InterviewWorkspace to AI Elements, ProjectList + root layout to shadcn + Tailwind. Zero server-side changes. `done` - - Requirements: → SPEC.md §Requirements #4 - - Assumptions: → SPEC.md §Assumptions A17 (validates) - - Decisions: → SPEC.md §Decisions D14 (completes — AI Elements adopted) - - Invariants respected: → SPEC.md §Invariants I1, I7, I8, I15, I17, I18 - - Acceptance: `npm run verify` passes; AI Elements render messages/reasoning/tool states; shadcn Card/Button on project list; zero changes to src/server/*, src/core/*, drizzle/* - - Branch: `ln/fe-558-ui-foundation` - - **Verification approach**: inner — `npm run verify` (lint, format, type-check, all tests, build). Outer — manual visual inspection of interview workspace and project list in dev mode. - -5. **Observer agent + entity persistence** `FE-537` — After each answered turn, core invokes a second agent call that extracts decisions and assumptions. Writes to decision/assumption tables with turn linkage and dependency edges. Core yields `observer-complete` DomainEvent **post-commit** (after SQLite transaction); SSE adapter emits as typed data part on existing chat stream (in-band sync per D22). Context builders upgraded to use `md-pen` for structured entity rendering (tables, checklists) in observer context. Agent pattern refactored: conductTurn() is thin sequencer, each agent is async generator composed via yield* (D27). Observer uses outputFormat for structured JSON extraction (D28). ResultMessage inspection for agent metrics (D29). `done` - - Requirements: → SPEC.md §Requirements #5 - - Assumptions: → SPEC.md §Assumptions A3, A4, A14 (validated by spike), A20, A24, A25 - - Decisions: → SPEC.md §Decisions D22 (in-band sync — observer-complete as data part), D26 (md-pen), D27 (agent generator composition), D28 (outputFormat), D29 (ResultMessage metrics) - - Invariants established: → SPEC.md §Invariants I20, I21, I22 - - Invariants respected: → SPEC.md §Invariants I1, I5, I6, I9, I10, I12, I13, I17, I19 - - Acceptance: 147 tests pass (24 new); agent pattern refactored; observer persists entities with turn linkage and dependency edges; observer-complete emitted post-commit; SSE adapter encodes as data-observer-result; observer errors non-fatal; context uses md-pen; agent-metrics emitted - - Branch: `ln/fe-537-observer-agent` - - **Verification approach**: inner — unit tests for entity writes with dependency edges, observer-complete DomainEvent emission post-commit, SSE adapter data-part encoding, sdk translateStreamEvents parity, observer-error non-fatality, agent-metrics shape. Middle — differential oracle from spike fixtures (deferred to manual testing). Outer — debug mode and fixture capture (deferred to slice 6). → SPEC.md §Oracle Strategy - -6. **Entity sidebar (read-only)** `FE-538` — React sidebar in interview workspace showing decisions and assumptions in categorized tabs. TanStack Query manages entity state via `useQuery`; cache invalidated on chat stream completion (status transition `streaming` → `ready`). Entities API at `GET /api/projects/:id/entities`. Note: `onData` → `setQueryData` bridge from D22 not used — AI SDK `useChat` doesn't expose an `onData` callback for custom data parts; status-based invalidation used instead. Dependency edge display and stale badges deferred (require slices 11/12 infrastructure). `done` - - Requirements: → SPEC.md §Requirements #6 - - Assumptions: → SPEC.md §Assumptions A21 (partially validated — status-based invalidation works; onData bridge not needed) - - Decisions: → SPEC.md §Decisions D22 (TanStack Query — yes; in-band onData sync — replaced with status-based invalidation) - - Invariants established: → SPEC.md §Invariants I23 - - Invariants respected: → SPEC.md §Invariants I9, I10, I14, I20, I21 - - Acceptance: 149 tests (2 new API tests); entities API returns decisions + assumptions; sidebar renders in categorized tabs; TanStack Query cache invalidated on stream completion; entities appear as interview progresses - - Branch: `ln/fe-538-entity-sidebar` - - Ref: → docs/design/BREADBOARD.md §UI Affordances → P2 Entity sidebar - - **Verification approach**: inner — unit tests for entity query on active path, stale badge computation. Middle — validate A21: `onData` → `setQueryData` updates sidebar without stale closure (if stale, fall back to parallel `EventSource`). Outer — manual visual inspection (entities render correctly, tabs work, stale badges appear). Debug mode overlay (observer extraction detail per-turn) should land here or in slice 5. → SPEC.md §Oracle Strategy (outer loop), §Acknowledged Blind Spots (cumulative graph integrity) - -## Phase 4: Full Interview - - + +- Spike: **Observer extraction fidelity** `FE-557` `done` — narrowly validated observer extraction for the original decisions/assumptions ontology (≥80% capture rate); broadened knowledge-layer extraction still needs follow-up coverage +- Spike: **Raw Anthropic SDK** `done` — invalidated A2, validated A26, led to D30 -### Slices + +3b. **Rich chat UI** `FE-541` `done` — I7 +4. **Structured interview: scope phase** `FE-554` `done` — I16 +4a. **Parts-based persistence + context builders** `FE-555` `done` — I17, I18, I19 +4b. **Structured interview: client UI** `FE-556` `done` — I17↑, I18↑ +4c. **UI foundation: shadcn/ui + Tailwind 4 + AI Elements** `FE-558` `done` +5. **Observer agent + entity persistence** `FE-537` `done` — I20, I21, I22 +6. **Entity sidebar (read-only)** `FE-538` `done` — I23 +6b. **AI SDK-native chat pivot** `FE-559` `done` — I21↑, I22↑, I23↑; core tools spike proven (A29) +6b1. **Workspace seam characterization oracle** `done` — I24, I25 + - Purpose: add a client integration harness around the interview workspace before the state-ownership refactor + - Coverage: initial hydration from persisted turns, same-project refresh stability, observer-result sidebar reactivity, option-selection follow-through + - Unblocks: 6c live streaming fix, workspace state-ownership refactor commits -7. **Phase transition + resolution** — Agent judges when scope phase is complete (`is_resolution`). Core yields `phase-resolved` DomainEvent. Client shows summary modal. User confirms to advance. Phase indicator updates. `not-started` - - Requirements: → SPEC.md §Requirements #7, #8 - - Assumptions: → SPEC.md §Assumptions A15 - - Acceptance: agent marks resolution, summary shows, user confirms, phase indicator reflects completion +## Phase 4: Interaction + Knowledge Foundations -8. **Design drill-down phase** — Second agent skill. Walks the design tree with structured questions. Decisions extracted by observer. Continues until agent judges resolution. `not-started` - - Requirements: → SPEC.md §Requirements #2, #3 - - Assumptions: → SPEC.md §Assumptions A13 (validated by slice 4) - - Acceptance: design questions with options, decisions extracted and shown in sidebar, agent resolves when understanding is reached + -9. **Requirements review phase** — Third agent skill. Walks accumulated requirements list. Agent checks for gaps, proposes additions. User confirms each. Requirements get `reviewed_at` stamped. `not-started` - - Requirements: → SPEC.md §Requirements #11 - - Assumptions: — - - Acceptance: agent presents requirements, suggests gaps, user confirms, reviewed_at updated +### Slices -10. **Criteria phase** — Fourth agent skill. For each confirmed requirement, agent proposes testable criteria. User selects/edits/confirms. Criteria get `reviewed_at` stamped. `not-started` - - Requirements: → SPEC.md §Requirements #12 - - Assumptions: — - - Acceptance: agent proposes criteria per requirement, user confirms, spec readiness predicate evaluable +6c. **Live streaming fix** — Fix the turn-card rendering regression: during live SSE streaming, the structured turn card does not render until page refresh. Thinking streams live; server persists correctly; hydration from DB works. Root cause is in the interaction between `toUIMessageStream()`, `useChat` part accumulation, and the tool-part lifecycle. `in-progress` + - Requirements: → SPEC.md §Requirements #2, #3, #4 + - Assumptions: → SPEC.md §Assumptions A16, A28 + - Candidate invariant goals: live tool-part rendering matches persisted state after refresh + - Invariants to respect: → SPEC.md §Invariants I16, I17, I18, I22 + - Invariants established: → SPEC.md §Invariants I43 + - Acceptance: send a message in dev, see the structured turn card appear live without refresh; `npm run verify` passes + - **Observed current state (2026-04-07, post-build):** the workspace controller now projects the latest streamed `tool-ask_question` input into the visible `TurnCard` before `onFinish` route invalidation, and targeted regression tests (`InterviewWorkspace`, `workspace-controller`, `workspace-data`, `app`) are green. The slice is still not safely retireable because manual browser verification is pending and `npm run verify` is currently blocked by unrelated repo-wide deprecation lint errors in `src/shared/chat.ts`, `src/client/components/ai-elements/prompt-input.tsx`, and `src/server/observer.ts`. + - **Observed code seam:** `InterviewWorkspace.renderParts()` still drops `tool-ask_question` transcript parts, but `workspace-controller-core.ts` now projects the latest streamed tool input into a temporary visible turn card while loading; durable loader state still owns the post-finish turn card after router invalidation. + - **Recommended next move for the implementing agent:** run a manual dev/browser walkthrough to confirm the turn card appears live in the real runtime, then retire 6c once the branch's unrelated lint baseline is resolved and `npm run verify` passes. + - **Verification approach**: inner — unit/integration tests for tool-part state transitions or alternate live render path. Outer — manual interview: turn card renders live, matches post-refresh state. + +6d. **Flexible turn-response model** — Replace the single-select answer assumption with typed response payloads that support zero/one/many selections, rationale, and custom answers. Keep structured interviewer guidance, recommendation, and strategic grounding, but stop assuming every turn maps to one categorical choice. `not-started` + - Requirements: → SPEC.md §Requirements #3, #6 + - Assumptions: → SPEC.md §Assumptions A16, A28 + - Decisions: → SPEC.md §Decisions D23, D24 + - Candidate invariant goals: turn-response payload round-trip fidelity; multi-select/custom-answer state hydrates and replays correctly + - Invariants to respect: → SPEC.md §Invariants I17, I18, I19, I22 + - Acceptance: a turn can be answered with multiple selections + rationale or with a custom answer; transcript, persistence, and resume stay aligned + - **Verification approach**: inner — schema + serialization tests for new prompt/response payloads. Outer — manual interview with multi-select and none-of-the-above answers. + +6e. **Generic knowledge layer schema + sidebar projection** — Introduce the broader semantic layer (`framing`, `constraint`, `decision`, `assumption`, `requirement`, `criterion`) with generic provenance and graph edges, then project it cleanly into the sidebar without regressing existing reads. `not-started` + - Requirements: → SPEC.md §Requirements #5, #6, #14 + - Assumptions: → SPEC.md §Assumptions A14 + - Decisions: → SPEC.md §Decisions D5, D13, D25 + - Candidate invariant goals: generic knowledge-item persistence with turn linkage; graph-edge fidelity across item kinds + - Invariants to respect: → SPEC.md §Invariants I20, I21, I23, I34 + - Acceptance: project state can load and display generic knowledge items and edges from the active path without losing current resume behavior + - **Verification approach**: inner — DB/core tests for generic item persistence and projection. Middle — workspace integration tests for sidebar hydration. + +6f. **Phase-aware observer extraction** — Teach the observer to bias extraction by mode: scope prefers framing/constraints, design prefers decisions/assumptions, later modes can surface requirements/criteria and revisions. Keep the observer as a single structured extraction pass, but give it richer context and a broader ontology. `not-started` + - Requirements: → SPEC.md §Requirements #5, #6, #11, #12 + - Assumptions: → SPEC.md §Assumptions A14, A20 + - Decisions: → SPEC.md §Decisions D4, D5, D13, D25 + - Candidate invariant goals: observer extracts framing without assumption inflation; phase-aware extraction deltas stay attributable to source turns + - Invariants to respect: → SPEC.md §Invariants I20, I21, I23 + - Acceptance: scope turns primarily yield framing/constraints; design turns primarily yield decisions/assumptions; observer results still stream in-band to the sidebar + - **Verification approach**: middle — golden-master observer fixtures across scope/design examples. Outer — inspect project 18–style sessions for ontology fit. + +## Phase 5: Mode Closure + Full Interview + + -## Phase 5: Revisit + Export +### Slices - +7. **Explicit phase outcomes + scope closure** — Replace pure `is_resolution` semantics with explicit phase outcomes and user-confirmed scope closure. Scope mode closes when framing sufficiency is reached, not just when the model feels done. `not-started` + - Requirements: → SPEC.md §Requirements #7, #8 + - Assumptions: → SPEC.md §Assumptions A15, A28 + - Decisions: → SPEC.md §Decisions D2, D3, D6 + - Candidate invariant goals: confirmed scope outcome survives refresh and invalidates correctly when upstream turns change + - Invariants to respect: → SPEC.md §Invariants I18, I24, I25 + - Acceptance: scope mode proposes closure with a summary, user confirms, explicit phase outcome persists, and the project shows updated workflow state + - **Verification approach**: inner — DB/core tests for phase outcome lifecycle. Outer — manual closure/confirmation walkthrough. + +8. **Design mode (commitment / exploration)** — Implement the second workflow mode on the new turn and knowledge model. The interviewer walks design forks; the observer captures decisions, assumptions, new constraints, and emerging requirements. `not-started` + - Requirements: → SPEC.md §Requirements #2, #3, #5, #6 + - Assumptions: → SPEC.md §Assumptions A14, A15, A28 + - Decisions: → SPEC.md §Decisions D2, D5, D6 + - Candidate invariant goals: mode transition preserves interview continuity; design-mode turns produce coherent decision/assumption graph growth + - Invariants to respect: → SPEC.md §Invariants I18, I19, I21, I22 + - Acceptance: after confirmed scope closure, the interview enters design mode; design turns yield coherent commitments and assumptions and can propose design closure + - **Verification approach**: inner — mode-transition/controller tests. Outer — manual design walkthrough from a confirmed scope session. + +9. **Requirements-review mode** — Synthesize the requirement set from the full knowledge layer, then let the user approve, edit, merge, reject, and add requirements through review turns. `not-started` + - Requirements: → SPEC.md §Requirements #6, #11, #13 + - Assumptions: → SPEC.md §Assumptions A15, A28 + - Decisions: → SPEC.md §Decisions D2, D5, D6 + - Candidate invariant goals: requirements are capture-anytime but review-complete only through explicit review state + - Invariants to respect: → SPEC.md §Invariants I18, I19, I21, I24 + - Acceptance: requirements-review mode presents a synthesized requirement set, records explicit approval/edit state, and can close only when in-scope requirements are resolved + - **Verification approach**: inner — review-state lifecycle tests. Outer — manual requirement review with approvals, edits, and missing-item additions. + +10. **Criteria-review mode** — Synthesize verification conditions from approved requirements plus any earlier criteria-like signals, then drive review turns until coverage is complete. `not-started` + - Requirements: → SPEC.md §Requirements #6, #12, #13 + - Assumptions: → SPEC.md §Assumptions A15, A28 + - Decisions: → SPEC.md §Decisions D2, D5, D6, D17 + - Candidate invariant goals: criteria verify requirements explicitly and track review completeness separately from requirement state + - Invariants to respect: → SPEC.md §Invariants I18, I19, I21, I24 + - Acceptance: criteria-review mode presents synthesized criteria, records explicit review state, and can close only when approved requirements have sufficient verification coverage + - **Verification approach**: inner — criterion/review edge tests. Outer — manual criteria review with edits and coverage checks. + +## Phase 6: Revisit + Export + + ### Slices -11. **Decision revisit: branch + checkout** — Click "revisit" on a decision in the sidebar → confirmation → `POST /api/projects/:id/branch` → HEAD moves to fork point → conversation rewinds → stale entities leave active path (path exclusion). Branch dropdown shows available branches. Checkout to switch. `not-started` - - Requirements: → SPEC.md §Requirements #9, #10 +11. **Generalized revisit: branch + readiness invalidation** — Revisit any earlier turn, not just a decision card. Branch from that turn, restore the interview there, and invalidate downstream phase outcomes / review state from the affected frontier. `not-started` + - Requirements: → SPEC.md §Requirements #9, #10, #13 - Assumptions: → SPEC.md §Assumptions A6 - - Decisions: → SPEC.md §Decisions D17 (path exclusion) - - Acceptance: revisit a decision, new branch created, interview resumes from fork point, checkout returns to previous path - - Ref: → docs/design/BREADBOARD.md §Wiring → Decision revisit - -12. **Entity lifecycle API** — CRUD + review + verify/falsify endpoints for sidebar writes. `PUT .../assumptions/:id` with action (verify/falsify/update) triggers flag propagation per D17. `PUT .../requirements/:id` cascades to criteria. `PUT .../requirements/:id/review` and `.../criteria/:id/review` stamp `reviewed_at`. `not-started` - - Requirements: → SPEC.md §Requirements #9, #11, #12 - - Decisions: → SPEC.md §Decisions D17 (flag propagation) - - Acceptance: falsify an assumption → dependent entities flagged; edit a requirement → criteria flagged; review stamps reviewed_at - - Ref: → docs/design/BREADBOARD.md §Code Affordances → Entity lifecycle - -13. **Spec export** — Render markdown spec from active path entities (decisions, assumptions, requirements, criteria). Export route (`/project/:id/export`) shows preview. Download button. Enabled only when spec readiness predicate is true (all phases resolved + reviewed). `not-started` + - Decisions: → SPEC.md §Decisions D1, D3, D17 + - Candidate invariant goals: active-path switch hides abandoned-branch readiness; downstream stale state is attributed to the correct frontier + - Invariants to respect: → SPEC.md §Invariants I9, I10, I24, I25 + - Acceptance: revisit a scope/design/review turn, new branch created, interview resumes from that point, and downstream closure/review state becomes stale until re-walked + - **Verification approach**: inner — branching + readiness invalidation tests. Outer — manual revisit across multiple modes. + +12. **Knowledge review lifecycle API + sidebar edits** — CRUD/review endpoints for the broader knowledge layer. Editing or reviewing items should be provenance-bearing and update readiness state without becoming invisible side mutations. `not-started` + - Requirements: → SPEC.md §Requirements #6, #11, #12, #13 + - Assumptions: → SPEC.md §Assumptions A14 + - Decisions: → SPEC.md §Decisions D5, D17 + - Candidate invariant goals: review/edit actions are reflected in both knowledge state and readiness state; sidebar writes are visible and recoverable + - Invariants to respect: → SPEC.md §Invariants I23, I36, I41, I42 + - Acceptance: edit/review framing, constraints, requirements, or criteria from the sidebar; affected readiness updates visibly and persists across refresh/resume + - **Verification approach**: inner — mutation + invalidation tests. Outer — manual sidebar edit/review walkthrough. + +13. **Spec export from the reviewed knowledge layer** — Render markdown export from active-path, reviewed knowledge items and explicit phase outcomes. Export is enabled only when the new readiness predicate is satisfied. `not-started` - Requirements: → SPEC.md §Requirements #13 - Assumptions: — - - Acceptance: complete all phases, navigate to export, markdown preview with all active-path entities, download .md file - - Ref: → docs/design/BREADBOARD.md §Places → P3 + - Decisions: → SPEC.md §Decisions D5, D17, D26 + - Candidate invariant goals: export reflects active-path reviewed knowledge only; readiness predicate gates export correctly + - Invariants to respect: → SPEC.md §Invariants I18, I21 + - Acceptance: complete all modes, satisfy review completeness, navigate to export, see markdown preview from the reviewed knowledge layer, download `.md` file + - **Verification approach**: inner — export projection tests. Outer — manual export after a full walkthrough and after a revisit-induced stale state. -## Phase 6: Distribution +## Phase 7: Distribution @@ -211,20 +178,22 @@ 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 - - Assumptions: → SPEC.md §Assumptions A8 (validated) - Decisions: → SPEC.md §Decisions D20 + - Candidate invariant goals: packaged launcher preserves working DB lifecycle and browser boot flow + - Invariants to respect: → SPEC.md §Invariants I1, I2, I4, I5 - Acceptance: `npx brunch` with key in scope opens working app ## Horizon - + - 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) -- Entity graph visualization (decision + assumption DAG view) +- Knowledge graph visualization (framing / constraints / decisions / requirements / criteria view) - Exploratory pathway (for projects where the goal itself is unclear) -- Multi-provider support via AI SDK server-side (if Claude Agent SDK becomes limiting) +- 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 ## Dependencies @@ -232,23 +201,28 @@ ``` -Phase 1: 1 (skeleton) ──→ 2 (SQLite) -Phase 2: 2 ──→ 3 (turn schema) ──→ 3c (Drizzle+core) ──→ 3d (routing) -Phase 3: 3c ──→ 3b (rich chat UI) ──→ 4 (scope server) ──→ 4a (parts+context) ──→ 4b (client UI) ──→ 4c (UI foundation) ──→ 5 (observer) - spike (observer fidelity) ──→ 5 - 3d + 5 ──→ 6 (entity sidebar) -Phase 4: 6 ──→ 7 (transitions) ──→ 8 (design) ──→ 9 (requirements) ──→ 10 (criteria) -Phase 5: 6 ──→ 11 (branching) - 6 ──→ 12 (entity lifecycle API) +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 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) + 7 ──→ 8 (design mode) ──→ 9 (requirements-review) ──→ 10 (criteria-review) +Phase 6: 7 ──→ 11 (generalized revisit) + 9 ──→ 12 (knowledge review lifecycle API) 10 ──→ 13 (export) -Phase 6: 13 ──→ 14 (npx + CLI) +Phase 7: 13 ──→ 14 (npx + CLI) ``` ### Parallelism opportunities -- ~~Slice 3b and 3d can proceed in parallel after 3c~~ (done — both landed) -- ~~Observer spike and slice 4 can proceed in parallel~~ (slice 4 server done — spike is next on critical path) -- Observer spike can proceed in parallel with 4a (parts persistence) -- Slice 7 (transitions) and 11 (branching) can start in parallel once slice 6 lands -- Slice 12 (entity lifecycle API) can proceed in parallel with slice 11 -- Slice 14 (npx) can start early with a basic launcher, completing after slice 13 +- 6c (live streaming fix) and design work on 6d (flexible turn-response model) are mostly independent if 6d does not need to rewrite the live tool-part rendering seam. +- 6e (generic knowledge layer) can begin in parallel with 6d after agreeing on the payload shape boundary. +- 11 (generalized revisit) can begin once explicit phase outcomes (7) exist; it does not need requirements/criteria review UX to start proving readiness invalidation mechanics. +- 14 (npx) can start early with a basic launcher, completing after slice 13 when the new export predicate stabilizes. diff --git a/memory/SELF_REVIEW.md b/memory/SELF_REVIEW.md new file mode 100644 index 00000000..78517e25 --- /dev/null +++ b/memory/SELF_REVIEW.md @@ -0,0 +1,711 @@ +# Self review — flow talkthrough findings + +_Date:_ 2026-04-07 +_Status:_ living session note; update as the talkthrough continues + +## Purpose + +Capture findings surfaced while pressure-testing the current Brunch runtime flow described in `memory/SPEC.md` and `memory/PLAN.md` against the implemented system and real project data. + +## Current findings + +### 1. Phase semantics and turn schema are currently misaligned + +**Observed** +- The implemented turn/question contract is globally shaped around `ask_question` with: + - `question` + - `why` + - `impact` + - `options[]` + - one recommended option +- This schema is enforced for every current phase by `structuredQuestionSchema` in `src/shared/chat.ts` and by phase prompts in `src/server/interview.ts`. +- The phase enum is `scope | design | requirements | criteria`, but the interaction primitive is currently the same for all phases. + +**Why this matters** +- The scope/kickoff phase appears conceptually different from the design/decision phase. +- Scope is primarily about framing: concept, goal, business context, domain, user need, and constraints. +- That makes the current question-with-options decision-oriented turn shape feel like an interview primitive being applied too broadly. + +**Implication** +- We likely need to separate: + - a generic `turn` record in persistence + - from phase-specific interaction shapes and extraction policies + +### 2. Scope phase appears misfit for decision/assumption extraction + +**Observed** +- The observer prompt in `src/server/observer.ts` is phase-agnostic and always extracts only: + - `decisions[]` + - `assumptions[]` +- It does not branch behavior by phase. +- The context builder also presents the current turn without phase-specific extraction guidance. + +**Inference** +- In scope, many user answers are better modeled as framing facts / context / problem statements rather than decisions or assumptions. +- Because the observer only knows how to emit decisions and assumptions, it is incentivized to reinterpret framing facts as assumptions. + +**Implication** +- Scope likely needs either: + 1. a different observer mode, or + 2. a different entity family for kickoff/framing outputs + 3. or both + +### 3. Project 18 shows assumption overproduction and weak decision/assumption differentiation + +**Observed from `brunch.db` project 18 (`post-react-refinements`)** +- Turns captured: 6, all in `scope` +- Extracted entities: + - decisions: 1 + - assumptions: 10 +- Single decision: + - `Build a minimal storybook-like component testing layer` +- Many assumptions are closer to paraphrased facts or inferred framing summaries than falsifiable assumptions, e.g.: + - `The primary users are developers focused on isolated component development and rapid iteration` + - `The team is using modern React tooling (Vite, Next.js, or similar) in their development workflow` + - `The team experiences friction in their component development process when testing different prop/state combinations` + +**Interpretation** +- The extractor is currently collapsing at least three distinct kinds of knowledge into `assumption`: + - direct user-provided facts + - contextual summaries + - actual falsifiable assumptions +- That makes the ontology blurry very early in the interview. + +### 4. Scope resolution is underdefined + +**Observed** +- `SPEC.md` defines phase resolution generically as `turn.is_resolution = true` when the model judges shared understanding has been reached. +- `PLAN.md` slice 7 adds a future `resolve_phase` tool and summary/confirmation UI. +- There is no explicit phase-specific completion contract yet for scope. + +**Inference** +- For scope, “shared understanding reached” is too vague unless we define the minimum framing bundle. + +**Candidate close condition for scope** +A scope phase could be considered ready to close only once the system has enough confidence in a framing bundle such as: +- concept / artifact being specified +- user / actor +- user need / pain +- business or workflow context +- core success shape or intended outcome +- major constraints / non-goals (if already known) + +### 5. The current model probably needs a stronger distinction between framing and commitment + +**Observed + inferred** +- Design phases are where `decision` and `assumption` seem most natural. +- Scope is where `framing context` seems natural. +- Current runtime and schema flatten both into one extraction model too early. + +**Implication** +- The product may want an explicit two-step epistemic progression: + 1. capture framing/context + 2. drill into decisions/assumptions + +### 6. The entity typology emerging in practice is richer than `decision` + `assumption` + +**Observed + inferred** +- The current schema has durable tables for `decision`, `assumption`, `requirement`, and `criterion`, but the observer currently extracts only decisions and assumptions. +- `SPEC.md` already says requirements "accumulate during the decision drill-down and are reviewed in a dedicated phase," which implicitly allows requirements to exist before the requirements-review phase. +- Session evidence and user feedback suggest the interview also surfaces additional knowledge kinds that are not well represented today, including: + - user stories / problem statements + - fixed preconditions / context facts + - constraints + - non-goals + - requirements emerging before the explicit requirements-review mode + +**Interpretation** +- The product ontology is currently too narrow for the information the interview naturally produces. +- `assumption` is absorbing multiple incompatible knowledge types because there are not enough semantic buckets. + +### 7. Phase boundaries may be correct for workflow mode while still being too rigid for entity capture + +**Observed + inferred** +- The application needs phases because each phase changes the interviewing mode, UI expectations, and closure logic. +- But the docs already partially weaken strict capture boundaries by stating that requirements can accumulate before the dedicated review phase. +- This suggests a more general rule: phases should govern how the system behaves, not strictly when an entity is allowed to first appear. + +**Implication** +- A better model may be: + - **capture-anytime** for entities that naturally surface during conversation + - **review-in-phase** for the phase that is responsible for confirming, editing, or closing that entity family +- Under this model: + - scope can surface early requirements, constraints, and non-goals + - design can surface additional framing facts and requirements + - requirements phase becomes the place where requirements are audited and confirmed, not necessarily first created + +### 8. `turn` should likely remain the provenance spine, but not the only semantic container + +**Observed** +- The current implementation already treats the turn tree as the core history structure: + - `turn.parent_turn_id` forms the tree + - `project.active_turn_id` is the HEAD pointer + - entities are linked back to turns through provenance join tables (`turn_decision`, `turn_assumption`) + - phase provenance currently lives on each turn (`turn.phase`) +- `SPEC.md` explicitly defines the turn tree as version history. + +**Inference** +- This is still the right center of gravity. +- What should change is not the centrality of `turn`, but how much semantic weight is forced directly into the turn row and its single global interaction schema. + +**Implication** +- A stronger model is: + - `turn` remains the chronological / branching spine of the interview + - phase outcomes, summaries, resolutions, and extracted entities attach to turns through explicit linked records or typed payloads + - phase-specific interaction contracts can vary without breaking the turn tree as the version-history backbone + +### 9. Revisitation semantics can likely be generalized around turn-local invalidation frontiers + +**Observed + inferred** +- The current model already uses turn-tree branching plus downstream invalidation for revisit behavior. +- The same logic can plausibly be extended beyond design decisions if phase outcomes and review states are attached to turns clearly enough. + +**Candidate revisit semantics** +- revisiting / editing **framing** in scope should unresolve scope and force re-walk of downstream design, requirements, and criteria readiness +- revisiting **design commitments** should branch from the relevant interview turn and force re-resolution of design plus downstream review phases +- editing **requirements** should invalidate criteria generation/review downstream of the edited requirement set +- editing **criteria** should invalidate only criteria review completeness unless the edit exposes a requirements mismatch + +**Design pressure this creates** +- We probably need explicit resolution records / predicates per phase rather than treating phase completion as only a boolean on the latest turn. +- We may also need to distinguish: + - conversational turns + - review/edit actions performed outside the main transcript +- Otherwise sidebar edits risk becoming second-class mutations that sit outside the main provenance story. + +### 10. Later phases should likely synthesize from the full knowledge layer rather than act as first-capture phases + +**Observed + inferred** +- The existing spec already allows requirements to accumulate before the explicit requirements-review phase. +- User feedback suggests the later phases are better understood as structured review/synthesis passes over the accumulated knowledge layer. + +**Candidate behavior spec** +- **Requirements-review mode**: + - gather tentative requirements already surfaced + - map/extrapolate from the broader knowledge layer (framing, constraints, decisions, assumptions, non-goals, problem statements) + - propose a fuller requirement set + - ask for approval, edits, merges, deletions, and missing items +- **Criteria-review mode**: + - gather any already-mentioned proofs / benchmarks / KPIs / acceptance signals + - map/extrapolate from both the knowledge layer and confirmed requirements + - propose a complete criteria set + - ask for approval and edits until criteria are review-complete + +**Implication** +- These later phases look more like normalization, completeness checking, and confirmation passes than like virgin capture phases. + +### 11. The current question/option UI shape is too narrow for real interview behavior + +**Observed in code** +- `structuredQuestionSchema` requires `options` and assumes one recommended option. +- `data-option-selection` carries a single `selectedOptionId` plus optional `rationale`. +- `selectOption()` in `src/server/db.ts` clears previous selections and marks exactly one option as selected. +- `TurnCard` in `src/client/routes/InterviewWorkspace.tsx` disables further option interaction once one choice has been made. + +**Observed from user testing + inferred implications** +- Some questions are multi-select in reality; more than one proposed option may apply. +- Users need stronger affordance to provide explanation / grounding along with selections. +- Users need a first-class way to reject all provided options and answer in free text. + +**Interpretation** +- The current turn UI assumes a categorical question with a single categorical answer. +- That assumption is too narrow for framing and likely too narrow for parts of design and review as well. + +**Implication** +- The turn interaction model likely needs to expand beyond single-select options into some combination of: + - multi-select + - explicit rationale capture + - none-of-the-above / custom answer + - possibly phase-specific response widgets + +### 12. The observer is intentionally a single extraction call with a different context projection than the interviewer + +**Observed** +- The interviewer is created via `ToolLoopAgent` in `src/server/interview.ts` and receives active-path conversational context via `buildInterviewerContext(...)`. +- The observer is not a looped agent. It is a single `generateObject(...)` call in `src/server/observer.ts`. +- The observer currently receives: + - the current turn + - existing extracted entities for the project + - `activePathSummary`, which is currently passed as an empty string from `runObserver(...)` +- `SPEC.md` explicitly documents this as a deliberate projection difference: interviewer gets conversational continuity, observer gets extraction context for incremental delta capture. + +**Why this design exists** +- The stated intent is to keep extraction incremental: “given what we already know, what did this turn add?” +- That makes the observer cheaper, simpler, and easier to validate than a second full conversational agent loop. + +**Current limitation** +- Although the design allows an active-path summary, the current implementation is not yet actually providing one (`activePathSummary: ''`). +- So today the observer does **not** receive the same history richness as the interviewer. + +**Implication** +- The split is principled, but the current observer context is also thinner than the design likely wants long-term. +- As the ontology broadens, the observer will probably need a richer, phase-aware context projection even if it remains a single structured extraction call. + +## Evidence reviewed + +- `memory/SPEC.md` +- `memory/PLAN.md` +- `src/server/interview.ts` +- `src/server/observer.ts` +- `src/server/context.ts` +- `src/shared/chat.ts` +- `brunch.db` project 18 and associated `turn`, `decision`, `assumption`, and join-table records + +## Proposed minimal expanded ontology + +This is the smallest broadened ontology that currently seems able to fit the interview without exploding complexity. + +### Durable entity families + +1. **framing** + - Purpose: capture the stable framing bundle that scope/kickoff is trying to establish. + - Typical examples: + - project goal + - user / actor + - user need / pain + - business or workflow context + - domain fact + - fixed precondition + - Why it exists: these are not well modeled as either decisions or assumptions. + +2. **constraint** + - Purpose: capture hard limits, exclusions, and boundaries on the solution space. + - Typical examples: + - must use React + - Anthropic-only + - single-user + - no collaborative editing + - no offline mode + - Why it exists: constraints shape downstream design but are not necessarily decisions and should not be hidden inside framing text. + +3. **decision** + - Purpose: capture actual commitments or chosen forks in the design tree. + - Typical examples: + - use SQLite + - use a turn tree for version history + - use AI SDK `ToolLoopAgent` + - Keep: rationale and dependency edges. + +4. **assumption** + - Purpose: capture beliefs that downstream choices rely on and that could prove false. + - Typical examples: + - observer latency is hidden under user think-time + - users arrive with a reasonably defined goal + - Why it remains: it is still useful, but should shrink back to a stricter semantic meaning. + +5. **requirement** + - Purpose: capture what the system must do. + - Note: capture-anytime, review-in-phase. + +6. **criterion** + - Purpose: capture how a requirement will be verified or recognized as satisfied. + - Note: capture-anytime, review-in-phase, but mostly synthesized/confirmed in criteria-review mode. + +### Deliberate omissions from the minimal set + +The following may still matter, but should probably start life as typed sub-kinds or annotations rather than first-class top-level tables: +- non-goal (could initially be a `constraint` subtype) +- problem statement / user story (could initially be a `framing` subtype) +- benchmark / KPI / proof signal (could initially be a `criterion` subtype) + +### Core rule + +- `decision` = chosen fork +- `assumption` = belief that could be wrong +- `framing` = contextual truth or intent statement +- `constraint` = boundary on the acceptable solution space +- `requirement` = must-do capability +- `criterion` = verifiable success condition + +## Proposed minimal expanded turn-response model + +The current single-select option turn is too narrow. The minimal response model should support structured guidance without forcing all answers into a single categorical choice. + +### Recommended response shapes + +1. **single-select with rationale** + - use when one best branch really should be chosen + - keep recommendation support + +2. **multi-select with rationale** + - use when several proposed options may all apply + - especially useful for constraints, priorities, applicable contexts, requirement bundles + +3. **custom answer / none-of-the-above** + - user can reject all offered options and provide free text + - should be first-class, not an accidental escape hatch through the global text box + +4. **approve / edit / reject review turn** + - for requirements-review and criteria-review modes + - better fit than pretending review is the same as exploratory interviewing + +### Minimal answer payload capabilities + +A turn answer should be able to express at least: +- selected option IDs: zero, one, or many +- free-text rationale / explanation +- custom free-text answer +- approval state for review-style turns + +### Recommended principle + +Keep the interviewer's habit of proposing structure, options, and recommendations — that is a product strength — but stop assuming that every answer is a single categorical pick. + +## Proposed schema shape + +### Recommendation: keep the four-layer model explicit in storage + +1. **History spine** → turn tree +2. **Workflow mode** → phase/mode on turns plus phase outcomes +3. **Knowledge layer** → generic knowledge items + provenance + edges +4. **Readiness layer** → phase outcomes + item review records + +### A. History spine + +#### `project` +Keep essentially as-is: +- `id` +- `name` +- `active_turn_id` +- timestamps + +#### `turn` +Refactor toward a more generic turn record. + +Recommended fields: +- `id` +- `project_id` +- `parent_turn_id` +- `phase` (`scope | design | requirements | criteria`) +- `kind` (`interaction | review | system | edit`) +- `prompt_payload` (JSON) +- `response_payload` (JSON, nullable until answered) +- `user_parts` (UI resume state) +- `assistant_parts` (UI resume state) +- timestamps + +**Key recommendation** +- Stop treating `question`, `why`, `impact`, `answer`, and normalized `option` rows as the primary durable schema. +- Those are really interaction payloads, not the stable semantic core of a turn. +- Keep them only as transitional projections if needed during migration. + +### B. Prompt / response payloads + +Use typed JSON payloads validated by Zod per turn kind. + +#### `choice` prompt payload +- `kind: "choice"` +- `selectionMode: "single" | "multi"` +- `question` +- `why` +- `impact` +- `options[]` +- `recommendedOptionIds[]` +- `allowCustomAnswer: boolean` +- `encourageRationale: boolean` + +#### `choice` response payload +- `selectedOptionIds[]` (zero / one / many) +- `rationale` (optional text) +- `customAnswer` (optional text) + +#### `review` prompt payload +- `kind: "review"` +- `targetKind` (`requirement | criterion | framing | constraint | decision | assumption`) +- `items[]` +- `reviewGoal` (`approve | edit | prune | fill-gaps`) +- `summary` (optional) + +#### `review` response payload +- `approvals[]` +- `edits[]` +- `rejections[]` +- `additions[]` +- `rationale` (optional) + +**Why JSON here** +- Turn interactions are mode-shaped and likely to evolve. +- Trying to normalize every interaction form into columns and child tables will overfit the current UI and make the schema brittle. + +### C. Knowledge layer + +#### `knowledge_item` +Replace multiple narrow entity tables with one generic durable table. + +Recommended fields: +- `id` +- `project_id` +- `kind` (`framing | constraint | decision | assumption | requirement | criterion`) +- `subtype` (nullable text) +- `content` +- `rationale` (nullable) +- timestamps + +**Why one table** +- The ontology is broadening. +- The provenance and dependency mechanics are structurally similar across these kinds. +- A generic table avoids six parallel CRUD paths, join tables, invalidation rules, and observer extraction codepaths. + +#### `turn_knowledge_item` +Generic provenance join: +- `turn_id` +- `item_id` +- `relation` (`captured | confirmed | edited | invalidated | reviewed`) + +This replaces specialized joins like `turn_decision` / `turn_assumption`. + +#### `knowledge_edge` +Generic dependency / derivation graph: +- `from_item_id` +- `to_item_id` +- `relation` (`depends_on | derived_from | constrains | verifies | refines`) + +Examples: +- decision `depends_on` assumption +- requirement `derived_from` decision or framing +- constraint `constrains` decision +- criterion `verifies` requirement + +### D. Readiness layer + +#### `phase_outcome` +Make phase closure explicit instead of encoding it only as `turn.is_resolution`. + +Recommended fields: +- `id` +- `project_id` +- `phase` +- `status` (`proposed | confirmed | superseded`) +- `source_turn_id` +- `confirmed_turn_id` (nullable) +- `invalidated_by_turn_id` (nullable) +- `summary` +- timestamps + +This gives each phase its own closure artifact tied back to the turn tree. + +#### `knowledge_review` +Represent review-in-phase explicitly. + +Recommended fields: +- `id` +- `item_id` +- `phase` +- `status` (`pending | approved | edited | rejected | stale`) +- `source_turn_id` +- `superseded_by_turn_id` (nullable) +- timestamps + +This replaces special-case `reviewed_at` fields on only some entity tables and supports the broader model where many entity kinds may be captured early but reviewed later. + +### E. What to de-emphasize or remove from the current schema + +If we move to the recommended shape, the following are either transitional or become obsolete: +- turn scalar columns: `question`, `why`, `impact`, `answer`, `is_resolution` +- `option` as a first-class normalized table for all turns +- narrow entity tables: `decision`, `assumption`, `requirement`, `criterion` +- specialized provenance joins: `turn_decision`, `turn_assumption` +- specialized edge tables like `decision_parent_decision`, `decision_parent_assumption`, `assumption_parent_assumption` +- special-case `reviewed_at` fields only on requirements / criteria + +### F. Why this is the right amount of generality + +This is intentionally more generic in storage than the current implementation, but not fully abstract or event-sourced everywhere. + +It keeps: +- one turn tree as the branching history +- one knowledge table as the semantic layer +- one edge table as the dependency graph +- one review mechanism for readiness + +That feels like the smallest schema that matches the architecture we are converging toward. + +## Proposed phase / mode behavior spec + +This section assumes the schema shape above: `turn` as history spine, `knowledge_item` as the semantic layer, and `phase_outcome` / `knowledge_review` as the readiness layer. + +### 1. Scope mode (framing) + +**Primary job** +- establish a usable framing bundle for the rest of the interview + +**Interviewer behavior** +- ask broad-to-focused framing questions +- prefer clarifying concept, actors, pain, workflow context, success shape, and major boundaries +- use structured options when helpful, but allow custom answers and mixed truths +- encourage rationale whenever the answer reveals priorities or tradeoffs + +**Observer behavior** +- preferentially capture: + - `framing` + - `constraint` + - early `requirement` when the user states a must-have directly +- avoid manufacturing `decision` or `assumption` unless the turn truly contains them + +**Primary artifacts expected by end of mode** +- framing bundle with enough coverage to begin meaningful design exploration +- identified hard constraints / non-goals +- possibly some seed requirements + +**Closure condition** +Scope is ready to propose closure when the knowledge layer contains a minimally sufficient framing bundle, likely covering: +- what is being built +- for whom +- why it matters / what pain exists +- operating context / domain / workflow +- major constraints or exclusions known so far +- enough clarity to pursue design without asking basic identity questions again + +**Readiness representation** +- interviewer proposes a `phase_outcome` for `scope` +- user confirms or edits +- confirmation creates / updates relevant `knowledge_review` records on framing and constraints + +### 2. Design mode (commitment / exploration) + +**Primary job** +- walk the design tree and turn framing into commitments, assumptions, and downstream implications + +**Interviewer behavior** +- ask branch-shaping design questions +- surface tradeoffs explicitly +- use single-select when one fork should be chosen +- use multi-select when several forces or applicable patterns may coexist +- encourage rationale strongly because explanation is part of the design substance + +**Observer behavior** +- preferentially capture: + - `decision` + - `assumption` + - newly surfaced `constraint` + - newly surfaced `requirement` + - occasional `framing` refinements when the user corrects or deepens earlier understanding +- attach graph edges across all relevant item kinds + +**Primary artifacts expected by end of mode** +- a coherent decision set +- explicit assumptions under key decisions +- requirement candidates derived from design consequences +- refined constraints and framing where needed + +**Closure condition** +Design is ready to propose closure when: +- the major design forks relevant to the current scope have been resolved enough to support requirement synthesis +- the remaining uncertainty is acceptable / local rather than architecture-shaping +- the current decision / assumption graph is coherent on the active path + +**Readiness representation** +- interviewer proposes `phase_outcome` for `design` +- user confirms the summary of commitments and open assumptions +- relevant `knowledge_review` records for decisions / assumptions can be marked approved or left pending where explicit follow-up is needed + +### 3. Requirements-review mode (audit / completeness) + +**Primary job** +- normalize, complete, and confirm the requirement set implied by the whole knowledge layer + +**Interviewer behavior** +- gather already-captured requirements +- synthesize additional requirements from framing, constraints, decisions, and assumptions +- present grouped or itemized review turns +- ask for approval, edits, merges, deletions, and missing requirements + +**Observer behavior** +- preferentially capture: + - new `requirement` + - edits/refinements to existing `requirement` + - supporting edges showing which knowledge items requirements were derived from +- capture newly surfaced constraints or framing only when they materially affect the requirement set + +**Primary artifacts expected by end of mode** +- deduped, reviewable requirement set +- explicit approval / edit trail for requirements +- stale or superseded requirement drafts clearly marked as such + +**Closure condition** +Requirements-review is ready to close when: +- the requirement set is complete enough for verification design +- each in-scope requirement is approved, edited into approval, or explicitly rejected +- obvious gaps / duplicates / contradictions have been handled + +**Readiness representation** +- `knowledge_review` for `requirement` items is the main readiness source of truth +- interviewer proposes `phase_outcome` for `requirements` once the requirement review state is sufficiently complete + +### 4. Criteria-review mode (verification) + +**Primary job** +- turn approved requirements into verifiable success conditions + +**Interviewer behavior** +- gather any already-captured criteria-like signals (benchmarks, proofs, KPIs, acceptance signals) +- synthesize candidate criteria from approved requirements plus the broader knowledge layer +- present criteria in review form for approval and edits +- challenge vague or non-verifiable criteria + +**Observer behavior** +- preferentially capture: + - new `criterion` + - edits/refinements to criteria + - `verifies` edges from criteria to requirements +- capture escalations when criteria review reveals a requirement is underspecified + +**Primary artifacts expected by end of mode** +- criteria attached to each approved requirement that needs verification +- explicit verification-facing success conditions rather than informal aspirations + +**Closure condition** +Criteria-review is ready to close when: +- every in-scope approved requirement has sufficient verification coverage +- criteria are concrete enough to evaluate success +- unresolved criteria disputes have either been settled or intentionally pushed back upstream + +**Readiness representation** +- `knowledge_review` for `criterion` items is the main readiness source of truth +- interviewer proposes `phase_outcome` for `criteria` once criteria coverage is complete enough for export + +### Cross-mode rules + +#### Capture-anytime / review-in-phase +- `framing`, `constraint`, `decision`, `assumption`, `requirement`, and `criterion` may surface in any conversational mode +- each mode remains responsible for closing the review state of the entity families it owns + +#### Escalation rule +- later modes may push work upstream if review exposes a defect in an earlier layer: + - criteria exposes bad requirement → requirements becomes stale + - requirements exposes bad design premise → design becomes stale + - design exposes bad framing → scope becomes stale + +#### Revisit rule +- revisiting a turn re-establishes the active path from that turn forward +- affected `phase_outcome` rows become superseded or invalidated based on turn frontier +- affected `knowledge_review` rows become `stale` where their source basis is no longer trusted + +#### Export readiness rule +A project is export-ready only when: +- the latest active-path `phase_outcome` for each mode is confirmed and not invalidated +- all in-scope `requirement` reviews are approved / resolved +- all in-scope `criterion` reviews are approved / resolved +- no upstream staleness remains unresolved on the active path + +## Open questions to continue pressure-testing + +1. What exact framing subtypes do we need immediately versus later? +2. Should `constraint` and `framing` be separate first-class tables, or one table with typed variants? +3. What is the minimum sufficient close condition for scope? +4. Should the observer be phase-aware? +5. Which entity families should be capture-anytime versus review-in-phase? +6. Are phases primarily workflow modes, extraction policies, or both? +7. Should review/edit actions become explicit turn-linked events rather than side mutations? + +## Early remediation directions + +These are not yet commitments — just plausible directions surfaced by the talkthrough. + +- Introduce phase-specific extraction policies instead of a single observer ontology. +- Add at least two new semantic buckets ahead of `decision` / `assumption`: `framing` and `constraint`. +- Keep `requirement` and `criterion` as durable entities, but treat them as capture-anytime and review-in-phase. +- Shift toward a **capture-anytime, review-in-phase** model so workflow phases do not artificially suppress entities that surface early. +- Define a phase-specific resolution checklist for scope instead of relying only on freeform LLM judgment. +- Expand the turn-response model to support multi-select, rationale capture, custom answers, and explicit review approvals/edits. +- Revisit the global structured-question contract so later phases can use review/confirmation semantics instead of forcing all interactions into the same question shape. diff --git a/memory/SPEC.md b/memory/SPEC.md index e27ff922..166d46db 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -9,25 +9,27 @@ ## Concept & Goal -Brunch is an AI-guided spec elicitation tool that turns natural-language project goals into structured specifications through a multi-phase interview. The interview is driven by an agent that relentlessly asks structured questions — each with options, a recommendation, and strategic grounding ("why this matters") — until shared understanding is reached. A second observer agent extracts decisions and assumptions from each turn, building a dependency graph. The output is a fire-and-forget specification document. +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** (framing), **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. The core data model: -- **Turn tree** — The conversation is a tree of turns (question + options + answer), not a flat log. Turns branch when a decision is revisited. The active path from HEAD determines the current state. The turn tree *is* the version history — no snapshots needed. -- **Decision graph** — Decisions are the atoms of the spec. Each is a resolved fork with options, a chosen path, and a rationale. Decisions depend on prior decisions and assumptions, forming a DAG. Revisiting a decision forks the turn tree and soft-invalidates downstream entities. -- **Assumption graph** — Assumptions are the falsifiable beliefs that decisions rest on. They have their own dependency structure (assumptions can rest on prior assumptions). -- **Requirements & criteria** — Downstream projections. Requirements accumulate during the decision drill-down and are reviewed in a dedicated phase. Criteria are proposed against confirmed requirements. +- **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. +- **Knowledge graph** — The interview can surface more than just decisions and assumptions. Durable knowledge kinds are: `framing`, `constraint`, `decision`, `assumption`, `requirement`, and `criterion`. These items can surface in any mode, link back to source turns, and connect through typed graph edges (`depends_on`, `derived_from`, `constrains`, `verifies`, `refines`). +- **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. The architecture (layered: db → core → adapters): - **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, interview orchestration, entity lifecycle, observer, phase management, export. `conductTurn()` returns `AsyncIterable` for streaming. No transport knowledge. -- **Agent engine**: Claude Agent SDK (`query()`) — tool use, MCP, session resume, subagents, permissions, rich streaming events. Each interview phase is an agent skill. Called by core, not by adapters. -- **Observer agent**: Separate extraction call after each turn — captures decisions, assumptions, and their dependency edges. Invoked by core after turn completion. -- **Web adapter**: Express.js translates `DomainEvent` stream to AI SDK UI Message Stream SSE. React + Vite + `@ai-sdk/react` `useChat` client. +- **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, D31) +- **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 entities +- **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`. ## Constraints & Non-goals @@ -45,97 +47,105 @@ The architecture (layered: db → core → adapters): ## 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 — framing questions establish context before the design drill-down -3. Each turn presents a question with ≥2 options, a recommendation, and a "why this matters" grounding block — the user sees the strategic significance of each fork +2. Start a new project and have the agent begin a structured interview — scope/framing questions establish context 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 decisions and assumptions from each answered turn, building the dependency graph in the background -6. See accumulated decisions, assumptions, requirements, and criteria in a dashboard as the interview progresses -7. The interviewing agent determines when shared understanding is reached and marks the phase resolved — interview length is emergent, not predetermined -8. Phase transitions show a summary and require user confirmation before moving on -9. Revisit any previous decision by navigating the turn tree — this forks a new branch and soft-invalidates dependent entities for re-review +5. The observer agent extracts typed knowledge items from each answered turn — `framing`, `constraint`, `decision`, `assumption`, `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 closure condition; the interviewing agent determines when closure can be proposed, but user confirmation is required to mark the mode resolved +8. Phase / mode transitions show a summary and require user confirmation before moving on +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 -11. The requirements review phase walks the accumulated requirements list, checks for gaps, and confirms completeness -12. The criteria phase proposes testable acceptance criteria against confirmed requirements -13. Export the spec as markdown when all phases are resolved — spec readiness is the compound predicate of phase resolution + requirements reviewed + criteria confirmed -14. Close the browser and resume later — the turn tree, decisions, and assumptions persist in SQLite -15. The project dashboard shows all projects with their phase completion status +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 completion status ## Assumptions -| # | Assumption | Confidence | Dependent decisions | Implicated slices | Validation approach | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| A1 | AI SDK's UI Message Stream SSE protocol is documented and stable enough to emit conformantly without importing AI SDK server-side | **validated** | D8 | Walking skeleton | Validated: skeleton emits conformant SSE, 15 tests pass | -| A2 | Claude Agent SDK `query()` with `includePartialMessages` provides all streaming event types needed for CLI-quality feedback | **validated** | D8 | Walking skeleton | Validated: adapter translates stream_event messages correctly | -| A3 | Separating interviewer from observer produces better interview quality than inline tool calling | high | D1 | Observer agent | Spike confirms extraction is viable as separate call; interviewer prompt stays clean. Full comparison deferred to slice 5 manual testing. | -| 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 in slice 5 with model switch. | -| A5 | `better-sqlite3` npm prebuilt binary works across macOS/Linux without native compilation issues | **validated** | D7 | SQLite foundation | Validated: installed on macOS without native compilation issues | -| A6 | Turn-tree branching in SQLite is sufficient for decision revisit and undo in a single-user tool | high | D7 | Turn tree | Validate with realistic branch/merge scenarios | -| A7 | Users arriving at the tool have a reasonably defined goal | medium | — | Scope phase | User testing; exploratory pathway deferred if false | -| A8 | A single Express port serving API + static assets is sufficient for npx distribution | **validated** | D10 | npx distribution | Validated: Vite proxy to Express works in dev; single port | -| A9 | TanStack AI is too immature for a deliverable (alpha, v0) | medium | D9 | — | Re-evaluate if AI SDK becomes constraining | -| A10 | The `useChat` hook can consume custom SSE without AI SDK server runtime | **validated** | D9 | Walking skeleton | Validated: useChat consumes custom SSE via DefaultChatTransport | -| A11 | Stateless `query()` with prompt-stuffed history is sufficient for multi-turn interviewing — SDK session persistence is unnecessary and undesirable | **validated** | D8, D12 | SQLite foundation | Validated: formatting history into prompt works. SDK sessions rejected as competing source of truth — opaque, machine-local, incompatible with portable data goals (atomic YAML / git-versionable). Turn tree is sole session model. | -| A12 | `useChat` hook accepts initial messages to hydrate conversation state from server-stored history | **validated** | D9 | SQLite foundation | Validated: `useChat` doesn't have `initialMessages` prop but `setMessages` works for hydration | -| A13 | Phase-specific interview behavior is achievable via system prompt switching + in-process MCP tools on `query()` — the SDK's formal `AgentDefinition` skill system is unnecessary | **validated** | D2 | Interview phases | Validated: slice 4 uses `getSystemPrompt(phase)` + `createInterviewMcpServer()` per turn; 88 tests pass. SDK `AgentDefinition` subagent system not used — simpler approach with less indirection. | -| A14 | A second-thread observer agent can reliably extract decisions, assumptions, and dependency edges from a single turn's Q&A | **validated** | D1 | Observer agent | Validated (spike): decisions 100% capture, assumptions semantically correct (~80% true semantic overlap). Edges not tested — deferred to slice 5. Use tool-based structured output and faster model (Haiku) in production. | -| A15 | The LLM can reliably judge when a phase interview has reached sufficient understanding (is_resolution) | medium | D3 | Phase resolution | Probe across varied project types; measure false-positive resolution rate | -| A16 | AI SDK `useChat` hook's `ToolUIPart` state machine (`input-streaming` → `input-available` → `output-available` / `output-error` / `approval-requested` → `approval-responded` / `output-denied`) models all permutations of pending, error, and success for both interim (thinking, tool calls) and final (response) data | high | D14 | Rich chat UI | Partially validated: SSE adapter emits tool-call events, client renders `dynamic-tool` parts with state labels (input-streaming, input-available, output-available, output-error). Browser outer-loop pending. | -| A17 | AI Elements copy-paste components can be restyled without forking — they are ownable source files, not npm-locked dependencies | **validated** | D14 | Rich chat UI | Validated: installed via `npx ai-elements@latest add`; components live at `src/client/components/ai-elements/*.tsx` as editable source; `@ai-sdk/react` is devDep only (not bundled); `ai` package provides types. No hidden runtime dependency. | -| A18 | Drizzle ORM migration runner reliably auto-applies schema changes from a migrations folder at startup with better-sqlite3 | **validated** | D18 | Drizzle refactor | Validated: migrate() auto-applies at startup in createDb(); all 39 existing tests pass against Drizzle-managed schema | -| A19 | `AsyncIterable` from core can be consumed by both SSE streaming (web) and line-by-line terminal output (CLI) without buffering issues | **validated** | D19 | Core extraction | Validated: conductTurn() yields DomainEvents consumed by Express SSE adapter; 12 new core tests + 9 app integration tests pass | -| A20 | Observer results can be delivered as typed data parts on the existing chat SSE stream without holding the connection open unacceptably long — observer is synchronous, runs within the same `conductTurn()` request, completes during user read time | high | D22 | Observer agent, Entity sidebar | Measure observer latency in slice 5; if >5s, fall back to out-of-band SSE (Option 2 in research doc) | -| A21 | `useChat` `onData` callback reliably bridges to `queryClient.setQueryData` without stale-closure issues — known `onFinish` stale-closure bug (ai-sdk#550) may or may not affect `onData` | medium | D22 | Entity sidebar | Test in slice 6: verify `setQueryData` from `onData` updates sidebar reactively; if stale, use parallel `EventSource` instead | -| A22 | AI SDK `UIMessage.parts[]` with custom Data Parts (typed via `dataPartsSchema`) persisted as JSON on the turn table is sufficient for faithful UI resume — no separate `turn_message` table needed for current scope | **validated** | D23, D24 | Parts persistence | Validated: parts assembler converts DomainEvents to typed parts, round-trips through JSON persistence (I18). Client hydration from parts deferred to 4b (outer-loop). | -| A23 | Custom Data Parts for structured user input (option selection, confirmation) can replace scalar `turn.answer` as the primary user-response model without breaking `formatHistory()` or observer context | **validated** | D24 | Parts persistence | Validated: Data Part schemas defined with Zod (I17), context builders read scalars not parts (I19), structured user input round-trip tested. Full UI wiring deferred to 4b. | -| A24 | SDK `outputFormat` with JSON schema produces equally reliable entity extraction as MCP tool-based extraction — structurally simpler (one API call, no tool round-trip), schema validation built into SDK response via `structured_output` field on `SDKResultMessage` | high | D28 | Observer agent | Validate in slice 5: compare extraction quality with outputFormat vs spike's MCP tool approach. If outputFormat produces malformed or lower-quality extraction, fall back to MCP tool pattern | -| A25 | `SDKResultMessage` provides accurate `duration_ms`, `total_cost_usd`, and `usage` for per-agent observability — types confirmed in TS SDK (`SDKResultSuccess`, `SDKResultError`) | high | D29 | Observer agent | Validate in slice 5: inspect ResultMessage after query() iteration, confirm fields are populated | + + +| # | Assumption | Confidence | Dependent decisions | Implicated slices | Validation approach | +| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A3 | Separating interviewer from observer produces better interview quality than inline tool calling | high | D1 | Observer agent | Spike confirms extraction is viable as separate call; interviewer prompt stays clean. | +| 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 | medium | D4, D5, D13 | Observer agent, Knowledge layer | Validated narrowly for decisions/assumptions; broadened ontology still needs probes across framing, constraints, requirements, and criteria-like signals. | +| A15 | The LLM can reliably judge when a workflow mode has reached sufficient closure to propose a phase outcome | medium | D3 | Phase resolution | Probe across varied project types; measure false-positive closure rate and user override frequency | +| A16 | AI SDK `useChat` hook's `ToolUIPart` state machine models all permutations of pending, error, and success for tool calls | high | D14 | Rich chat UI, 6c live streaming fix | Partially validated: typed `tool-ask_question` parts render with correct state labels, and live tool parts project into the visible turn card before route invalidation in `workspace-controller.test.tsx` and `InterviewWorkspace.test.tsx`. Browser outer-loop pending. | +| 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` covers `data-observer-result` → query invalidation → sidebar refresh, plus manual outer-loop verification remains for live browser/runtime behavior. | +| 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 | D31 | Agent loop, Phase transitions | Validate with mode-transition and review slices: agent must ask, synthesize, and propose closure without a handwritten loop. | +| A29 | Models can reliably compose generic filesystem tools (read, write, edit, bash, grep, find, ls) to explore and characterize an existing project | **validated** | D32 | Characterization kickoff | Validated (spike): `ToolLoopAgent` with 7 core tools explored brunch in 22 tool calls across 23 steps. See `spike/filesystem-tools.ts`. | +| A30 | The client can detect when assistant content actually needs rich markdown or diagram enhancement and keep plain text rendering as the immediate default without creating a hydration or streaming mismatch | **validated** | D34, D36 | Refactor commit 4 — progressive rendering split | Validated by `src/client/capabilities/markdown-rendering.test.tsx` (plain path stays immediate, fenced code upgrades after lazy load) plus `src/client/build-boundary.test.ts` (entry excludes `streamdown` and eager highlighter implementation). | +| A31 | A workspace data adapter can centralize the boundary between durable project snapshots, durable entity snapshots, and ephemeral chat state without changing current user-visible behavior before concurrency and hydration policy changes land | **validated** | D37 | Refactor commits 5-7 — workspace state ownership | Validated by `src/client/workspace/workspace-data.test.ts` (durable vs ephemeral seed state separation is explicit and hydration timing is not owned by the adapter) plus unchanged green `src/client/routes/InterviewWorkspace.test.tsx` characterization coverage. | +| A32 | A project-scoped workspace loader can start durable project and entity snapshots together while seeding the entity query cache without reintroducing transcript hydration drift | **validated** | D38 | Refactor commit 6 — workspace loading concurrency | Validated by `src/client/routes/InterviewWorkspace.test.tsx` (initial sidebar data comes from the route loader with no post-mount entity fetch, same-project durable refresh updates sidebar state without rewriting the visible transcript, and observer-result invalidation still refetches entities through the same query boundary). | ## Decisions -27. **Agent module pattern — generator composition** — Each agent (interviewer, observer, future phase agents) is an async generator function yielding `DomainEvent`s. `conductTurn()` is a thin sequencer composing agents via `yield*`. No wrapper around `query()` — each agent calls the SDK directly with whatever options it needs (`outputFormat`, `effort`, `mcpServers`, etc.). A shared `translateStreamEvents()` utility in `sdk.ts` maps SDK `stream_event` messages to DomainEvents; streaming agents use it, silent agents don't. File layout: `interviewer.ts` (evolves from `interview.ts`), `observer.ts` (new), `sdk.ts` (new). Research: `docs/research/claude-agent-sdk-cookbook-patterns-vs-brunch-usage.md`. Depends on: D19. Supersedes: monolithic `conductTurn()` with inline `query()` call and stream parsing. +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). -28. **Observer uses `outputFormat` (structured JSON output)** — The observer agent returns extracted entities via SDK `outputFormat` with a Zod-derived JSON schema, not via MCP tools. The SDK validates the response and places the parsed result in `SDKResultMessage.structured_output`. This is simpler than tool-based extraction (one API call, no tool round-trip) and better suited to the observer's pure-extraction job (no side effects during the call). The interviewer retains MCP tools because `ask_question` has side effects (DB writes during the call). Depends on: A24. Supersedes: MCP tool-based observer extraction from spike. +31. **`ToolLoopAgent` as the agent loop** — AI SDK's built-in `ToolLoopAgent` provides the tool execution loop: model calls tool → SDK validates input via Zod → executes handler → re-submits result → repeats until `stopWhen` condition or `end_turn`. No custom `agentLoop()` function needed. `activeTools` and `prepareCall` enable per-step tool gating for future phase-specific behavior. Depends on: D30. Supersedes: planned custom agent loop modeled after pi-mono. -29. **`ResultMessage` inspection for agent observability** — After each `query()` call, the agent inspects `SDKResultMessage` for `duration_ms`, `duration_api_ms`, `total_cost_usd`, and `usage`. Emitted as `agent-metrics` DomainEvent. Primary use: validate A4 (observer latency) and track cost per turn. Secondary: surface in future debug mode overlay. Depends on: A25. Supersedes: discarding `ResultMessage` (gap #1 in cookbook research). +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, A29. 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. +33. **Component-level workspace oracle before state refactors** — The interview workspace has a client integration harness (`InterviewWorkspace.test.tsx`) that uses the real React Query cache and component tree while mocking `useChat` transport boundaries. It locks four seam behaviors before state-ownership refactors: initial hydration from persisted turns, same-project refresh preserving local chat state, `data-observer-result` invalidating entities into the sidebar, and option selection flowing through route refresh and chat submission. Depends on: D19, D22. Supersedes: manual-only workspace seam verification. -### Domain model +34. **Heavy client capabilities live behind named boundaries before perf changes** — Streamed markdown rendering, reasoning rendering, code highlighting, and the developer debug route are each imported through dedicated client boundary modules (`src/client/capabilities/*`, `src/client/routes/debug-surface.tsx`) rather than directly from feature components. This keeps runtime behavior unchanged now while giving later refactor commits one place to introduce lazy loading, deferred enhancement, or alternative adapters without another cross-cutting import rewrite. Depends on: D9, D14. Supersedes: direct heavy-dependency imports from message, reasoning, code-block, and router modules. -1. **Turn tree as version history** — The conversation is a tree, not a flat log. Each turn points to its parent. Revisiting a decision forks a new branch. `project.active_turn_id` is the HEAD pointer. The active path determines which entities are current — no snapshot tables needed. Depends on: A6. Supersedes: D5-old snapshot versioning model. -2. **Interview phases as agent skills** — Each phase (scope, design, requirements, criteria) is a separate agent skill with its own system prompt and tool configuration. The server orchestrates which skill to invoke based on phase completion state. Phases can be composed, reordered, or replaced independently. Depends on: A13. Supersedes: —. -3. **Phase resolution via LLM judgment** — A turn's `is_resolution` flag is set by the interviewing agent when it judges that shared understanding has been reached for that phase. The active path is resolved for a phase when its latest turn has `is_resolution = true`. Spec export requires all phases resolved. Depends on: A15. Supersedes: —. -4. **Two-agent pattern (interviewer + observer)** — The interviewer focuses solely on conducting the interview with structured questions. After each answered turn, a separate observer agent extracts decisions, assumptions, and dependency edges. The observer can use a cheaper/faster model. Keeps the interviewer prompt clean and extraction independently testable. Depends on: A3, A4, A14. Supersedes: —. -5. **Decision dependency graph** — Decisions depend on prior decisions and/or assumptions via `decision_parent_decision` and `decision_parent_assumption` join tables. Assumptions can depend on prior assumptions via `assumption_parent_assumption`. The observer agent captures these edges during extraction. Depends on: A14. Supersedes: —. -6. **Soft invalidation for requirements and criteria** — When a decision is revisited (branch fork), requirements traced to that decision are flagged for re-review via stale `reviewed_at` timestamps. Criteria inherit the flag transitively from their requirements. Mechanism specified in D17. Depends on: —. Supersedes: —. +35. **Developer debug surface is route-lazy, not startup-eager** — The `/debug` route remains declared in the main router, but its UI loads through a lazy client boundary so the default interview entrypoint does not inline developer-only debug content into the initial application chunk. This keeps the route available without charging normal startup for the debug surface. Depends on: D9, D34. Supersedes: eager debug-route component loading from the main router. -17. **Two invalidation mechanisms — path exclusion and flag propagation** — Path exclusion (lazy): `revisitDecision` → `branch()` moves HEAD; entities on the abandoned branch leave the active path. Requirements are stale when their source decision is not on the active path — computed by the active-path query, no eager writes. Flag propagation (eager): `falsifyAssumption` walks dependency graph edges (`assumption_parent_assumption`, `decision_parent_assumption`), marks dependents. `updateRequirement` nulls `reviewed_at` on traced criteria. Cascade model: falsify assumption → walk graph → flag dependents; revisit decision → branch → path exclusion; update requirement → flag criteria. Depends on: D1, D5, D6. Supersedes: D6's unspecified "holistic" re-qualification. +36. **Assistant rich rendering is progressive enhancement, not the baseline path** — Message and reasoning text render immediately through a plain text-safe boundary. Rich markdown, diagram rendering, and Shiki-backed highlighting load only after the content proves enhancement is needed, with the rich implementation and highlighter runtime emitted outside the default entry bundle. Depends on: D14, D34. Supersedes: startup-eager `streamdown` + highlighting on the default transcript path. -12. **Stateless SDK integration — no session persistence** — Each `query()` call uses `persistSession: false`. Conversation context is reconstructed from the turn tree's active path and injected as formatted history + structured entity summaries. SDK sessions (`resume`, `fork`, session IDs) are not used. The turn tree is the sole session model. Rationale: SDK sessions are an opaque, machine-local competing source of truth incompatible with brunch's branching semantics and future portable-data goals (atomic YAML, git-versionable). Depends on: A11. Supersedes: implicit reliance on SDK session state. -13. **Observer captures derived intelligence** — The observer agent's extraction mandate extends beyond decisions and assumptions to include derived observations (e.g. codebase analysis, domain insights) that the interviewer surfaced through tool use during a turn. These are persisted so subsequent stateless `query()` calls can inject them as context. The exact entity model is TBD — candidates include a dedicated `observation` table, enriched `decision.rationale`, or a `notes` field on `turn`. Depends on: A14, D12. Supersedes: —. +37. **Workspace state ownership lives behind a data adapter before semantics change** — The client reads workspace data through an explicit adapter that separates durable project snapshots, durable entity snapshots, and ephemeral chat seed state. This commit preserves current behavior, including the current project-scoped chat hydration boundary, while giving later commits one place to change fetch concurrency and hydration policy without another cross-cutting rewrite. Depends on: D19, D22. Supersedes: inline workspace ownership logic spread across `InterviewWorkspace` and `EntitySidebar`. -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. -15. **~~Transitional turn-field inversion~~** — **Superseded by D23 (parts-based persistence)**. Previously: `turn.answer` held user text, `turn.question` held assistant text with inverted semantics during slices 1–3. This was always marked transitional. D23 replaces both scalar fields with persisted `UIMessage.parts[]` as the source of truth for UI rendering and resume. Scalar fields (`question`, `why`, `impact`, `answer`) retained for queryability only — domain queries (active path, phase filtering, entity joins) read scalars; UI hydration reads parts. Depends on: D1. Supersedes: flat `message` table with `role` field from slice 2. +38. **Workspace route loading is the project-scoped durable-data entrypoint** — The interview route loader now starts project and entity snapshot fetches together, then seeds the entity query cache from that loader result so the sidebar can render from the same project entry boundary without a post-mount waterfall. Later observer-result invalidations still refetch through the entity query key, while same-project loader refreshes can update durable snapshots without implicitly rewriting the visible transcript. Depends on: D9, D22, D37. Supersedes: project-only route loading plus post-mount entity fetch from the sidebar path. + +39. **Chat hydration is an explicit workspace boundary policy** — Persisted turns seed `useChat` only on initial project entry or when navigation changes the active project. Same-project route invalidations may refresh durable project/entity snapshots and derived affordances, but they do not rewrite the current in-flight transcript. The policy lives in a dedicated client boundary instead of being inferred indirectly from adapter memoization. Depends on: D19, D37, D38. Supersedes: implicit project-id-keyed hydration behavior hidden inside workspace adapter wiring. + +40. **Client writes use a shared typed mutation boundary with visible failure states** — Project creation, option selection, and similar client-triggered writes go through one shared POST-mutation helper plus React Query mutation state. Server `error` payloads are surfaced as visible UI feedback instead of being swallowed by silent early returns, while successful writes keep their existing navigation or route-refresh follow-through. Depends on: D22, D37, D39. Supersedes: ad hoc `fetch` calls in route components with inconsistent error handling. + +41. **Render-sensitive client primitives use explicit lifecycle boundaries** — Code highlighting now uses an effect-owned async path with cache reads kept synchronous and side-effect-free, message-branch bookkeeping re-synchronizes when branch identity changes and clamps stale indices when branch sets shrink, and transient copy-feedback timers are cleared explicitly on replacement or unmount. Depends on: D34, D39, D40. Supersedes: render-time state resets, callback-style async highlighting orchestration, and branch bookkeeping that only tracked collection length. -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. Turn table gains `user_parts` and `assistant_parts` JSON columns (nullable). On stream finish, core assembles final assistant `parts[]` from DomainEvents and persists alongside scalar fields. Hydration reads persisted parts when available, falls back to scalar synthesis for older turns. The turn tree remains canonical for domain semantics (branching, phase, entity joins); parts are the source of truth for rendering. 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. +42. **Advanced rendering boundaries expose explicit preload surfaces without contaminating first paint** — Markdown and code-highlighting capabilities now export preload hooks so pointer, focus, or touch intent can warm rich rendering before full use, while the transcript keeps the plain path during active animation and the build oracle enforces both chunk separation and a default-entry size ceiling. Depends on: D34, D35, D36, D41. Supersedes: lazy-only enhancement with no intent-preload or budget guardrail. -24. **Custom Data Parts for structured user input** — User responses are not always plain text. AI SDK Data Parts (`data-{name}` typed via Zod schema) model structured user input: `data-option-selection` (`{ turnId, selectedOptionId, rationale? }`), `data-confirmation` (`{ turnId, confirmed: boolean }`), plain `text` for freeform responses. Defined as a `BrunchDataParts` type passed as generic to `UIMessage` for full-stack type safety. Assistant messages use the same mechanism for domain-specific content not covered by built-in part types: `data-phase-summary`, `data-observer-result`, `data-entity-snapshot`. Depends on: A22, A23. Supersedes: implicit assumption that `turn.answer` is always a text string. +43. **Workspace orchestration reads through one controller boundary backed by a pure core and imperative shells** — Route components now consume a single workspace controller interface, while durable-state shaping, transcript seeding, and view projection live in pure functions and React Query/chat side effects live in dedicated shells. Depends on: D37, D38, D39. Supersedes: workspace ownership spread across route components plus loosely coordinated helper modules. -25. **Typed context builders replace monolithic `formatHistory()`** — Different consumers of the turn tree need different projections of the same data. `buildInterviewerContext(activePath, currentInput, entities, phase)` for conversational continuity. `buildObserverContext(turn, activePathSummary, linkedEntities)` for extraction-optimized context (see §Observer History Projection). Future: `buildPhaseResolutionContext(...)`, `buildRequirementsReviewContext(...)`. Each builder reads from the domain model (turn scalars + entity tables), NOT from persisted `UIMessage.parts[]`. The parts are for rendering; context builders are for inference. Depends on: D23, D12. Supersedes: single `formatHistory()` function in core.ts. +44. **Domain-shaped client mutations own success choreography above the shared transport seam** — `client-mutation.ts` remains the shared POST/error boundary, but project creation and turn-option selection now flow through domain hooks that own navigation, invalidation, and chat follow-through so route/controller callsites do not repeat workflow logic. Depends on: D40, D43. Supersedes: route- or controller-local success choreography on top of the generic mutation helper. + +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. +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 readiness model** — A phase is not truly resolved because one turn was marked special; it is resolved because the active path has a confirmed, non-invalidated phase outcome for that mode. 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: —. +5. **Generic knowledge graph replaces the decision/assumption-only model** — The durable semantic layer is a typed knowledge graph: `framing`, `constraint`, `decision`, `assumption`, `requirement`, and `criterion`, with subtype support where needed. Items link back to source turns and connect through typed edges (`depends_on`, `derived_from`, `constrains`, `verifies`, `refines`). Supersedes: separate decision / assumption graph as the sole semantic core. +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 framing sufficiency, 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: framing change → 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`. +13. **Observer captures typed knowledge items plus derived intelligence** — The observer's extraction mandate extends beyond decisions and assumptions to include framing facts, constraints, emerging requirements, criteria-like signals, and derived observations that the interviewer surfaced during the turn. These 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. +24. **Custom Data Parts model structured user responses beyond single-select choice** — User responses are not always plain text or one categorical pick. AI SDK Data Parts model structured input such as zero/one/many option selections, rationale, custom answer overrides, confirmations, and later review actions. Assistant messages use the same mechanism for domain-specific output such as phase summaries, observer results, and entity snapshots. Depends on: A22, A23. Supersedes: implicit assumption that `turn.answer` is always a text string and that every structured answer is a single selected option. +25. **Typed context builders are phase-aware projections over history + knowledge + readiness** — Different consumers of the turn tree need different projections of the same underlying state. `buildInterviewerContext(...)` provides conversational continuity. `buildObserverContext(...)` provides extraction-optimized context over the current turn plus accumulated knowledge and relevant history summary. Future builders include readiness / review projections for phase-outcome proposal and review modes. Each builder reads from the domain model, not from persisted `UIMessage.parts[]`. Supersedes: single `formatHistory()` function in core.ts. ### Technical stack -7. **SQLite via better-sqlite3** — Zero-config embedded DB. Turn tree, decisions, assumptions, requirements, criteria all in SQLite tables. Schema defined in Drizzle (see D18). Depends on: A5, A6. Supersedes: Dolt (docker-based). -8. **Express.js server emits AI SDK-conformant SSE** — Thin adapter: iterates `DomainEvent` stream from `conductTurn()`, translates each event to AI SDK UI Message Stream protocol via `createDomainAdapter()`. No AI SDK runtime imported server-side. The SDK is called by core, not by Express. Depends on: A1, A2, D19. Supersedes: hand-rolled NDJSON streaming, direct SDK iteration in Express (pre-3c). +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/design/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. -11. **Drop list** — Dolt/mysql2, OpenCode sidecar, Preact, both existing frontend implementations, NDJSON protocol, JSON Schema definitions (→ Zod), @tanstack/react-table, @dnd-kit/, dompurify, marked, four streaming functions in claude.js, dispatch.js. Depends on: —. Supersedes: —. 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 DomainEvent streaming** — Core interview orchestration extracted from Express handlers into interface-agnostic service layer. Core operations: turn tree (createProject, conductTurn, getActivePath, branch, checkout), entity lifecycle (revisitDecision, falsifyAssumption, verifyAssumption, CRUD for requirements/criteria, reviewRequirement/reviewCriterion), observer (runObserver), phase (getPhaseStatus), export (exportSpec). `conductTurn()` returns `AsyncIterable` — domain events (`stream-start`, `thinking`, `text-delta`, `tool-call-start`, `tool-call-delta`, `tool-call-end`, `stream-end`, `turn-created`, `error`, `observer-complete`; future: `phase-resolved`) that each adapter translates to its transport format. `observer-complete` is emitted post-commit (after SQLite transaction) and carries created entity IDs for cache coherence (see D22). Web (Express+SSE), CLI, and MCP adapters are thin transport layers. Depends on: A19, D8, D12. Supersedes: interview logic embedded in Express POST handler. +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 + SSE-driven invalidation for observer entity sync** — Observer-created entities (decisions, assumptions, edges) sync to the React UI via two mechanisms: (1) **In-band data parts** (default): `conductTurn()` yields `observer-complete` DomainEvents after the SQLite transaction commits; the Express SSE adapter emits these as typed data parts on the existing chat stream; `useChat`'s `onData` callback bridges to `queryClient.setQueryData` for instant sidebar updates. (2) **Out-of-band SSE** (fallback): if the observer moves to async post-processing, a dedicated `/api/events/:projectId` `EventSource` in a React context drives `queryClient.invalidateQueries`. TanStack Query owns all persisted entity state; a small Zustand store handles transient UI state only (observer-running indicator, phase progress). TanStack DB evaluated and rejected — overkill for server-authoritative single-user app without offline, multi-tab, or complex cross-collection query needs. Research: `docs/research/async-server-state-to-ui-sync-for-chat-observer-agents.md`. Depends on: A20, A21, D4, D9, D19. Supersedes: —. +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. ## Invariants @@ -145,31 +155,51 @@ The architecture (layered: db → core → adapters): Established by ln-build/ln-spike traceability. Referenced by PLAN.md slices (to establish / to respect). --> -| # | Invariant | Established by | Protected by | Proves | -| --- | ---------------------------- | ------------------- | -------------------------------- | ------- | -| I1 | SSE protocol conformance | Slice 1 (skeleton) | sse-adapter.test.ts | D8 | -| I2 | Stream lifecycle correctness | Slice 1 (skeleton) | app.test.ts | D8 | -| I3 | Thinking/text separation | Slice 1 (skeleton) | sse-adapter.test.ts, app.test.ts | D8 | -| I4 | Vite proxy routing | Slice 1 (skeleton) | vite.config.ts (manual) | D10 | -| I5 | DB lifecycle correctness | Slice 2 (SQLite) | db.test.ts | D7 | -| I6 | Turn persistence | Slice 3 (turn tree) | db.test.ts, app.test.ts | D1, D7 | -| I7 | Tool call SSE conformance | Slice 3b (rich UI) | sse-adapter.test.ts | D8, D14 | -| I8 | Tool part state rendering | Slice 3b (rich UI) | manual (outer loop) | D14 | -| I9 | Turn tree parent chain | Slice 3 (turn tree) | db.test.ts | D1 | -| I10 | Active path resolution | Slice 3 (turn tree) | db.test.ts | D1 | -| I11 | Drizzle migration auto-apply | Slice 3c (Drizzle) | db.test.ts | D18 | -| I12 | DomainEvent streaming | Slice 3c (Drizzle) | core.test.ts | D19 | -| I13 | Core/adapter separation | Slice 3c (Drizzle) | core.test.ts, app.test.ts | D19 | -| I14 | Project-scoped API routes | Slice 3d (routing) | app.test.ts | D9 | -| I15 | Route loader hydration | Slice 3d (routing) | manual (outer loop) | D9 | -| I16 | Schema validation on agent tool output | Slice 4 (scope interview) | interview.test.ts | D2, A13 | -| I17 | Data Part schema validation | Slice 4a (parts persistence) | parts.test.ts (7 tests) | D24 | -| I18 | Parts round-trip fidelity | Slice 4a (parts persistence) | parts.test.ts (8 tests), core.test.ts | D23 | -| I19 | Context builder equivalence | Slice 4a (parts persistence) | context.test.ts (7 tests) | D25 | -| I20 | Entity persistence with turn linkage | Slice 5 (observer) | db.test.ts (7 tests), observer.test.ts | D4, D5 | -| I21 | Observer-complete post-commit | Slice 5 (observer) | observer.test.ts (6 tests), sse-adapter.test.ts (3 tests) | D22 | -| I22 | Agent generator composition | Slice 5 (observer) | core.test.ts, sdk.test.ts (7 tests) | D27 | -| I23 | Entity sidebar reactive update | Slice 6 (sidebar) | app.test.ts (2 tests), manual (outer loop) | D22 | +| # | Invariant | Established by | Protected by | Proves | +| --- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------- | +| I1 | SSE protocol conformance | Slice 1 (skeleton) | app.test.ts | D8 | +| I2 | Stream lifecycle correctness | Slice 1 (skeleton) | app.test.ts | D8 | +| I3 | Thinking/text separation | Slice 1 (skeleton) | app.test.ts | D8 | +| I4 | Vite proxy routing | Slice 1 (skeleton) | vite.config.ts (manual) | D10 | +| I5 | DB lifecycle correctness | Slice 2 (SQLite) | db.test.ts | D7 | +| I6 | Turn persistence | Slice 3 (turn tree) | db.test.ts, app.test.ts | D1, D7 | +| I7 | Tool call SSE conformance | Slice 3b (rich UI) | app.test.ts, manual (outer loop) | D8, D14 | +| I8 | Tool part state rendering | Slice 3b (rich UI) | manual (outer loop) | D14 | +| I9 | Turn tree parent chain | Slice 3 (turn tree) | db.test.ts | D1 | +| I10 | Active path resolution | Slice 3 (turn tree) | db.test.ts | D1 | +| I11 | Drizzle migration auto-apply | Slice 3c (Drizzle) | db.test.ts | D18 | +| I12 | Typed server chat boundary | Slice 3c (Drizzle) | core.test.ts, app.test.ts | D19 | +| I13 | Core/adapter separation | Slice 3c (Drizzle) | core.test.ts, app.test.ts | D19 | +| I14 | Project-scoped API routes | Slice 3d (routing) | app.test.ts | D9 | +| I15 | Route loader hydration | Slice 3d (routing) | manual (outer loop) | D9 | +| I16 | Schema validation on agent tool output | Slice 4 (scope interview) | interview.test.ts | D2, A13 | +| I17 | Data Part schema validation | Slice 4a (parts persistence) | parts.test.ts (7 tests) | D24 | +| I18 | Parts round-trip fidelity | Slice 4a (parts persistence) | parts.test.ts (8 tests), core.test.ts | D23 | +| I19 | Context builder equivalence | Slice 4a (parts persistence) | context.test.ts (7 tests) | D25 | +| I20 | Entity persistence with turn linkage | Slice 5 (observer) | db.test.ts (7 tests), observer.test.ts | D4, D5 | +| I21 | Observer-result in-band sync | Slice 5 (observer) | observer.test.ts, app.test.ts | D22 | +| I22 | AI SDK-native interviewer path | Slice 6b (AI SDK pivot) | app.test.ts, interview.test.ts | D30, D31 | +| I23 | Entity sidebar reactive update | Slice 6 (sidebar) | app.test.ts, manual (outer loop) | D22 | +| I24 | Workspace hydration boundary stability | Slice 6b1 (workspace oracle) | InterviewWorkspace.test.tsx | D19, D22 | +| I25 | Workspace event bridge correctness | Slice 6b1 (workspace oracle) | InterviewWorkspace.test.tsx | D9, D22 | +| I26 | Progressive code-render fallback | Refactor commit 1 (client characterization coverage) | code-block.test.tsx | D14 | +| I27 | Equal-length branch replacement stability | Refactor commit 1 (client characterization coverage) | message.test.tsx | D14 | +| I28 | Client build boundary observability | Refactor commit 1 (client characterization coverage) | build-boundary.test.ts | — | +| I29 | Heavy client dependency indirection | Refactor commit 2 (client capability boundaries) | capability-boundaries.test.ts | D34 | +| I30 | Default entry excludes debug surface code | Refactor commit 3 (lazy debug route boundary) | build-boundary.test.ts | D35 | +| I31 | Assistant transcript rendering stays text-first until enhancement is needed | Refactor commit 4 (progressive rich rendering split) | markdown-rendering.test.tsx | D36 | +| I32 | Default entry excludes rich rendering and eager highlighting implementation | Refactor commit 4 (progressive rich rendering split) | build-boundary.test.ts | D36 | +| I33 | Workspace state ownership is explicit even while current hydration semantics are preserved | Refactor commit 5 (workspace data adapter) | workspace-data.test.ts, InterviewWorkspace.test.tsx | D37 | +| I34 | Workspace project and entity snapshots enter together through one project-scoped loader boundary | Refactor commit 6 (workspace loading concurrency) | InterviewWorkspace.test.tsx | D38 | +| I35 | Persisted chat state hydrates only on initial project entry or explicit project navigation | Refactor commit 7 (explicit chat hydration policy) | InterviewWorkspace.test.tsx, chat-hydration.test.ts | D39 | +| I36 | Client-triggered writes surface consistent visible failure states instead of silent no-ops | Refactor commit 8 (shared client mutations) | InterviewWorkspace.test.tsx, ProjectList.test.tsx | D40 | +| I37 | Code highlighting upgrades from lifecycle-owned async work and ignores stale completions during prop churn | Refactor commit 9 (render-sensitive primitive purity) | code-block.test.tsx | D41 | +| I38 | Message branch navigation stays aligned with the current branch set after replacement or shrink | Refactor commit 9 (render-sensitive primitive purity) | message.test.tsx | D41 | +| I39 | Advanced rendering boundaries expose intent-preload seams while keeping animated transcript content on the plain first-paint path | Refactor commit 10 (intent preloading + performance guardrails) | markdown-rendering.test.tsx, code-block.test.tsx, capability-boundaries.test.ts | D42 | +| I40 | The default client entry remains under an explicit size budget while excluding debug and rich-rendering payloads | Refactor commit 10 (intent preloading + performance guardrails) | build-boundary.test.ts | D42 | +| I41 | Workspace controller behavior is protected below the route boundary for loader seeding and same-project refresh stability | Refactor commit 14 (controller seam oracles) | workspace-controller.test.tsx | D43 | +| I42 | Shared client mutation transport reports network, non-JSON, and malformed-success failures consistently | Refactor commit 14 (mutation seam oracles) | client-mutation.test.ts | D44 | +| I43 | Live `tool-ask_question` parts project into the visible turn card before durable route refresh | Slice 6c (live streaming fix) | InterviewWorkspace.test.tsx, workspace-controller.test.tsx | D14, D43 | ## Lexicon @@ -190,39 +220,45 @@ The architecture (layered: db → core → adapters): ### Domain terms -| Term | Definition | -| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **project** | A spec elicitation session. Has a name, a HEAD pointer (`active_turn_id`), and phase completion state | -| **turn** | One question-answer pair in the interview. Carries phase provenance, options, grounding ("why"), impact signal, and the user's answer. Points to its parent turn — the turn tree is the version history | -| **option** | A structured alternative presented in a turn. At least two per turn. One may be recommended; one is selected by the user | -| **decision** | A resolved fork in the design tree. Extracted by the observer from an answered turn. Depends on prior decisions and/or assumptions. Traced back to its source turn via `turn_decision` | -| **assumption** | A falsifiable belief a decision rests on. Extracted by the observer. Can depend on prior assumptions. Traced back to its source turn via `turn_assumption` | -| **requirement** | What the system must do. Accumulated during the design drill-down, confirmed during the requirements review phase. Traced to source decisions via `requirement_decision`. Has `reviewed_at` for soft-invalidation | -| **criterion** | A testable condition verifying a requirement. Proposed by the agent during the criteria phase, confirmed by the user. Has `reviewed_at` for soft-invalidation | -| **active path** | The branch from HEAD to root in the turn tree. Determines which turns, decisions, and assumptions are currently active | -| **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 | -| **phase** | A stage of the interview: `scope`, `design`, `requirements`, `criteria`. Immutable provenance on each turn. Each phase is implemented via `getSystemPrompt(phase)` + a per-turn MCP tool server (`createInterviewMcpServer`). See D2, A13 | -| **phase resolution** | LLM judgment that shared understanding has been reached for a phase. Marked by `turn.is_resolution = true` on the last turn of a phase | -| **ask_question tool** | The MCP tool the interviewer must use each turn. Accepts `{ question, why, impact, options[] }`, validated by `structuredQuestionSchema` (Zod). The tool handler persists structured data to the turn and options tables via closure over `db` + `turnId`. Defined in `interview.ts` | -| **interview MCP server** | A per-turn MCP server created by `createInterviewMcpServer(db, turnId)`. Exposes the `ask_question` tool. The closure captures the current turn ID so the tool handler writes to the correct row. Passed to `query()` via `mcpServers` option. Defined in `interview.ts` | -| **interviewer** | The primary agent role: conducts the interview with structured questions, grounding, and impact signals. Must use the `ask_question` tool every turn. Does not extract entities | -| **observer** | The secondary agent role: extracts decisions, assumptions, and dependency edges from each answered turn. Runs post-answer during user read time | -| **core** | The interface-agnostic service layer between the database and transport adapters. Owns interview orchestration, entity lifecycle, observer invocation. Returns `AsyncIterable` for streaming | -| **domain event** | A typed event yielded by `conductTurn()` — `stream-start`, `thinking`, `text-delta`, `tool-call-start`, `tool-call-delta`, `tool-call-end`, `stream-end`, `turn-created`, `error`, `observer-complete`. Future: `phase-resolved`. Each adapter translates to its transport format (SSE, terminal, MCP). `observer-complete` is emitted post-commit and drives cache coherence (D22) | -| **decision graph** | The DAG of decisions and their dependencies (on prior decisions and assumptions). Revisiting a decision forks the turn tree | -| **path exclusion** | Invalidation by moving HEAD so entities on the abandoned branch leave the active path. Lazy — computed by the active-path query, no eager writes. Triggered by `revisitDecision` / `branch` | -| **flag propagation** | Invalidation by walking dependency graph edges and marking entities stale (nulling `reviewed_at`). Eager — triggered by `falsifyAssumption` or `updateRequirement` | -| **soft invalidation** | Umbrella term for both path exclusion and flag propagation. Entities are flagged for re-review but never deleted or modified. See D17 | -| **spec readiness** | Compound predicate: all four phases resolved AND requirements reviewed AND criteria confirmed. Only then is export enabled | -| **UIMessage** | AI SDK source of truth for UI state. `{ id, role, parts[], metadata? }`. Persisted for faithful resume. Reconstructed from stored `user_parts`/`assistant_parts` JSON on hydration. See D23 | -| **ModelMessage** | AI SDK representation optimized for LLM inference. Derived at call time by context builders (D25), never persisted. Leaner than `UIMessage` — no tool states, no reasoning, no custom data parts | -| **parts[]** | Ordered array of typed content blocks in a `UIMessage`. Built-in types: `text`, `reasoning`, `tool-{name}` (4 states), `file`. Custom types via Data Parts: `data-option-selection`, `data-confirmation`, `data-phase-summary`, etc. Source of truth for rendering. See D23, D24 | -| **Data Part** | Custom typed `UIMessage` part (`data-{name}`) defined via Zod schema. Enables structured user input (option selection, confirmation) and domain-specific assistant output (phase summary, observer result). Persisted in `parts[]` JSON. See D24 | -| **context builder** | A typed function that projects turn-tree + entity data into inference context for a specific consumer (interviewer, observer, phase judge). Reads from domain model, not from persisted parts. See D25 | -| **in-band sync** | Observer entity updates delivered as typed data parts on the existing `conductTurn()` SSE stream. Default mechanism — zero additional infrastructure (D22) | -| **out-of-band sync** | Observer entity updates delivered via a dedicated `EventSource` SSE channel (`/api/events/:projectId`). Fallback mechanism if observer becomes async (D22) | -| **cache invalidation** | Signaling TanStack Query that cached data is stale. Two forms: `queryClient.setQueryData` (push new data directly into cache) and `queryClient.invalidateQueries` (trigger background refetch). Driven by `observer-complete` events (D22) | +| 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. | +| **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, rationale, and custom answers. | +| **review turn** | A review interaction turn where the interviewer asks the user to approve, edit, reject, merge, or add to a synthesized item set. | +| **framing** | A contextual truth or intent statement: project goal, actor, user need, workflow context, domain fact, or problem statement. | +| **constraint** | A boundary on the acceptable solution space, including hard limits, exclusions, and non-goals. | +| **decision** | A chosen fork or commitment in the design tree. Depends on earlier knowledge and can carry rationale. | +| **assumption** | A falsifiable belief that downstream choices rely on and that could prove false. | +| **requirement** | A must-do capability of the system. Can surface in any mode, but is normalized and confirmed in requirements review. | +| **criterion** | A verifiable success condition for a requirement. Can surface early, but is normalized and confirmed in criteria review. | +| **knowledge item** | A generic semantic record in the knowledge layer. Durable kinds are `framing`, `constraint`, `decision`, `assumption`, `requirement`, and `criterion`, with subtype support where needed. | +| **knowledge graph** | The graph of knowledge items linked by typed edges such as `depends_on`, `derived_from`, `constrains`, `verifies`, and `refines`. | +| **capture-anytime** | Rule that any knowledge kind may surface in any conversational mode. | +| **review-in-phase** | Rule that each mode owns the closure / approval of the item families it is responsible for. | +| **phase outcome** | The explicit readiness artifact for a workflow mode: a proposed, confirmed, or superseded closure record tied back to the turn tree. | +| **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. | +| **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. | +| **spec readiness** | Compound predicate: each mode has a confirmed, non-invalidated phase outcome, all in-scope requirements and criteria are review-complete, and no unresolved upstream staleness remains. | +| **UIMessage** | AI SDK source of truth for UI state. `{ id, role, parts[], metadata? }`. Persisted for faithful resume. Reconstructed from stored `user_parts`/`assistant_parts` JSON on hydration. See D23. | +| **ModelMessage** | AI SDK representation optimized for LLM inference. Derived at call time by context builders (D25), never persisted. Leaner than `UIMessage` — no tool states, no reasoning, no custom data parts. | +| **parts[]** | Ordered array of typed content blocks in a `UIMessage`. Built-in types: `text`, `reasoning`, `tool-{name}`, `file`. Custom types via Data Parts: structured selections, confirmations, summaries, observer results, review actions, etc. Source of truth for rendering. See D23, D24. | +| **Data Part** | Custom typed `UIMessage` part (`data-{name}`) defined via Zod schema. Enables structured user input and domain-specific assistant output. Persisted in `parts[]` JSON. See D24. | +| **context builder** | A typed function that projects turn-tree + knowledge + readiness data into inference context for a specific consumer (interviewer, observer, phase closure, review modes). Reads from the domain model, not from persisted parts. See D25. | +| **in-band sync** | Observer knowledge updates delivered as typed data parts on the existing chat SSE stream. Default mechanism — zero additional infrastructure (D22). | +| **out-of-band sync** | Observer knowledge updates delivered via a dedicated `EventSource` SSE channel (`/api/events/:projectId`). Fallback mechanism if observer becomes async (D22). | +| **cache invalidation** | Signaling TanStack Query that cached data is stale. In the current web path, `useChat` `onData` invalidates the entity query from observer results, while route invalidation refreshes durable project state on stream completion. | +| **ToolLoopAgent** | AI SDK's built-in agent class that manages the model → tool-call → execute → re-submit loop. Powers the interviewer. Configured with `tools`, `stopWhen`, `providerOptions`. Methods: `generate()` (non-streaming), `stream()` (streaming). See D31. | +| **generateObject** | AI SDK function for structured output. Takes a Zod schema and returns a validated object. Powers the observer's extraction pass. See D30. | +| **core tools** | 7 generic filesystem tools (read, write, edit, bash, grep, find, ls) in `src/server/tools/`. Factory: `createCoreTools(cwd)`. Follow pi-mono's pattern. See D32. | +| **BrunchUIMessage** | `UIMessage` — the typed message contract spanning server validation, persistence, SSE streaming, and client hydration. Defined in `src/shared/chat.ts`. | ## Verification Design @@ -246,21 +282,21 @@ Verification is not a phase that follows implementation — it is integral to ev Scored per the arc-oracle diagnostic framework (high / partial / low): -| Dimension | Score | Notes | -| --- | --- | --- | -| **Observability** | partial | Inner/middle: high (all text-native — tests, SSE, DB). Outer: low for observer extraction quality (hidden from surface UI) and LLM judgment calls (phase resolution, interview quality). Mitigated by debug mode (planned) and differential testing (spike). | +| Dimension | Score | Notes | +| ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Observability** | partial | Inner/middle: high (all text-native — tests, SSE, DB). Outer: low for observer extraction quality (hidden from surface UI) and LLM judgment calls (phase resolution, interview quality). Mitigated by debug mode (planned) and differential testing (spike). | | **Reproducibility** | partial | Deterministic systems (turn tree, DB, SSE encoding): high. LLM boundary (interviewer output, observer extraction): low — non-deterministic. Mitigated by schema validation (structural) and golden master fixtures with capture-rate thresholds (statistical). | -| **Controllability** | high | Single-user, local SQLite, no external dependencies beyond Claude API. Agent drives full inner loop autonomously (`npm run fix` / `npm run verify`). Human review reserved for outer loop. | +| **Controllability** | high | Single-user, local SQLite, no external dependencies beyond Claude API. Agent drives full inner loop autonomously (`npm run fix` / `npm run verify`). Human review reserved for outer loop. | ### Verification Commands -| Step | Check | Command | -| ---- | ------------------ | ----------------------------------------- | -| 1 | Formatting | `npm run fmt:check` | -| 2 | Lint + type check | `npm run lint` | -| 3 | Unit tests | `npm run test` | -| 4 | Build | `npm run build` | -| all | Full pipeline | `npm run verify` | +| Step | Check | Command | +| ---- | ----------------- | ------------------- | +| 1 | Formatting | `npm run fmt:check` | +| 2 | Lint + type check | `npm run lint` | +| 3 | Unit tests | `npm run test` | +| 4 | Build | `npm run build` | +| all | Full pipeline | `npm run verify` | Tooling: oxfmt (formatting), oxlint + tsgolint (lint + type-aware + type-check), vitest (tests), vite (build). Replaces eslint + `tsc --noEmit`. @@ -276,41 +312,41 @@ End-to-end slices must be **user-testable**, not just programmatically tested. E **Inner loop** (ms–seconds): agent-autonomous, always-on -| Oracle family | What it proves | Protects | Cost | -| --- | --- | --- | --- | -| Schema validation | Agent tool output conforms to structured turn schema (question, options, grounding, impact) | I16 (planned) | Negligible — Zod parse on tool output | -| Fast unit tests — SSE | `SDKMessage` → correct SSE event strings | I1, I3, I7 | ms | -| 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 — observer sync | `observer-complete` emitted post-commit with entity IDs matching DB state; SSE adapter encodes as typed data part | D22, A20 | ms | -| Type-aware linting | Semantic static checks (oxlint + tsgolint) | All | ms | +| Oracle family | What it proves | Protects | Cost | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ------------------------------------- | +| Schema validation | Agent tool output conforms to the active turn/review payload schema for the current mode | I16 (planned) | Negligible — Zod parse on tool output | +| Fast unit tests — SSE | `SDKMessage` → correct SSE event strings | I1, I3, I7 | ms | +| 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 — observer sync | `observer-complete` emitted post-commit with entity IDs matching DB state; SSE adapter encodes as typed data part | D22, A20 | ms | +| Type-aware linting | Semantic static checks (oxlint + tsgolint) | All | ms | **Middle loop** (seconds–minutes): regression gates -| 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 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) | Full `conductTurn()` with observer → `observer-complete` is last event before `stream-end` → entity IDs in event match committed DB rows | D22 | seconds; requires Claude API | +| 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 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) | Full `conductTurn()` with observer → `observer-complete` is last event before `stream-end` → entity IDs in event match committed DB rows | D22 | seconds; requires Claude API | **Outer loop** (minutes–hours): human observer -| Oracle family | What it proves | Cost | -| --- | --- | --- | -| Debug mode (observer visibility) | Observer extraction is inspectable per-turn during manual testing | UI delta on slice 5/6 | -| Manual interview walkthrough | Structured questions render correctly; interview quality is acceptable | Human time | -| 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` → `setQueryData` bridge updates sidebar after observer extraction; validates A21 | Human + `/cli-cdp` (slice 6) | +| Oracle family | What it proves | Cost | +| -------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------- | +| Debug mode (observer visibility) | Observer extraction is inspectable per-turn during manual testing | UI delta on slice 5/6 | +| Manual interview walkthrough | Structured questions render correctly; interview quality is acceptable | Human time | +| 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` → `setQueryData` bridge updates sidebar after observer extraction; validates A21 | Human + `/cli-cdp` (slice 6) | ### Observer History Projection -The observer and interviewer receive the same conversation but through different projections. The interviewer receives conversational context ("where are we in the design space"). The observer receives extraction context: the existing entity graph (decisions, assumptions, edges established so far) plus the current turn's Q&A. This makes each extraction incremental — "given what we already know, what did *this turn* add?" — which sharpens the differential oracle: comparing the delta, not the total. +The observer and interviewer receive the same conversation but through different projections. The interviewer receives conversational context ("where are we in the design space"). The observer receives extraction context: the existing knowledge graph (knowledge items, edges, and relevant readiness hints established so far) plus the current turn's Q&A. This makes each extraction incremental — "given what we already know, what did *this turn* add or revise?" — which sharpens the differential oracle: comparing the delta, not the total. This projection difference is a deliberate design choice, not an implementation detail. It affects prompt design, fixture structure, and evaluation criteria. @@ -319,43 +355,52 @@ This projection difference is a deliberate design choice, not an implementation -| Blind spot | Why uncovered | Mitigation | Revisit trigger | -| --- | --- | --- | --- | -| Interview quality | LLM judgment; no programmatic oracle. Skill paradigm (D2) is the primary quality lever. | Manual outer-loop testing. | If interview quality proves inconsistent across project types. | -| 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. | -| Phase transition UX | Summary quality, resolution timing, confirmation flow. Fully visual. | Manual testing during slices 7-10. | If phase transitions feel wrong during testing. | -| 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 | Client-side `useChat` `onData` → `queryClient.setQueryData` bridge cannot be tested in inner/middle loop (requires browser runtime). Known `onFinish` stale-closure bug (ai-sdk#550) may affect `onData`. | Manual outer-loop validation in slice 6; 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). | +| Blind spot | Why uncovered | Mitigation | Revisit trigger | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Interview quality | LLM judgment; no programmatic oracle. Skill paradigm (D2) is the primary quality lever. | Manual outer-loop testing. | If interview quality proves inconsistent across project types. | +| 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. | +| Phase transition UX | Summary quality, resolution timing, confirmation flow. Fully visual. | Manual testing during slices 7-10. | If phase transitions feel wrong during testing. | +| 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). | ### Current Coverage -| File | Tests | Protects | -| ------------------- | ----- | --------------------------- | -| sse-adapter.test.ts | 8 | I1, I7, I21 | -| db.test.ts | 32 | I5, I6, I9, I10, I11, I18, I20 | -| app.test.ts | 22 | I2, I3, I6, I7, I13, I14 | -| core.test.ts | 16 | I12, I13, I18, I22 | -| interview.test.ts | 16 | I16 | -| parts.test.ts | 23 | I17, I18 | -| context.test.ts | 8 | I19 | -| sdk.test.ts | 7 | I22 | -| observer.test.ts | 6 | I20, I21 | +| File | Tests | Protects | +| ----------------------------- | ----- | -------------------------------------- | +| db.test.ts | 32 | I5, I6, I9, I10, I11, I20 | +| app.test.ts | 6 | I1, I2, I3, I7, I14, I21, I23 | +| core.test.ts | 6 | I12, I13, I18 | +| interview.test.ts | 6 | I16 | +| parts.test.ts | 7 | I17, I18 | +| context.test.ts | 8 | I19 | +| observer.test.ts | 2 | I20, I21 | +| InterviewWorkspace.test.tsx | 7 | I24, I25, I23, I33, I34, I35, I36, I43 | +| ProjectList.test.tsx | 2 | I36 | +| workspace-data.test.ts | 4 | I33 | +| chat-hydration.test.ts | 3 | I35 | +| workspace-controller.test.tsx | 3 | I41, I43 | +| client-mutation.test.ts | 3 | I42 | +| code-block.test.tsx | 4 | I26, I37, I39 | +| markdown-rendering.test.tsx | 3 | I31, I39 | +| message.test.tsx | 2 | I27, I38 | +| build-boundary.test.ts | 1 | I28, I30, I32, I40 | +| capability-boundaries.test.ts | 2 | I29, I39 | ## 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 with structured turns (question + options + grounding + impact) -3. The observer extracts decisions and assumptions from each answered turn, visible in the dashboard -4. The decision dependency graph is navigable — user can see what each decision depends on -5. Phase transitions show a summary, require user confirmation, and mark `is_resolution` -6. Revisiting a decision forks the turn tree and soft-invalidates downstream requirements +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 in the dashboard +4. The knowledge graph is navigable — user can see what each important item depends on, derives from, constrains, or verifies +5. Each workflow mode proposes closure with a summary, requires user confirmation, and records an explicit phase outcome +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 -8. Requirements review phase walks the list, agent suggests gaps, user confirms -9. Criteria phase proposes testable conditions for each requirement -10. Export produces valid markdown spec when all phases are resolved and entities reviewed -11. Closing and reopening the browser resumes the interview from the active turn +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 diff --git a/package-lock.json b/package-lock.json index 3fcdd70f..6836c97f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,9 @@ "name": "@hashintel/brunch", "license": "(MIT OR Apache-2.0)", "dependencies": { + "@ai-sdk/anthropic": "^3.0.66", "@ai-sdk/react": "^3.0.145", - "@anthropic-ai/claude-agent-sdk": "^0.2.77", + "@anthropic-ai/sdk": "^0.82.0", "@fontsource-variable/geist": "^5.2.8", "@modelcontextprotocol/sdk": "^1.27.1", "@radix-ui/react-use-controllable-state": "^1.2.2", @@ -34,7 +35,6 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "shadcn": "^4.1.2", "shiki": "^4.0.2", "streamdown": "^2.5.0", "tailwind-merge": "^3.5.0", @@ -44,16 +44,20 @@ "zod": "^4.3.6" }, "devDependencies": { + "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", "@types/express": "^5.0.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/supertest": "^7.2.0", + "agent-tail": "^0.4.0", "concurrently": "^9.2.1", "drizzle-kit": "^0.31.10", + "happy-dom": "^20.8.9", "oxfmt": "^0.43.0", "oxlint": "^1.58.0", "oxlint-tsgolint": "^0.19.0", + "shadcn": "^4.1.2", "supertest": "^7.2.2", "tsx": "^4.21.0", "typescript": "^5.9.3", @@ -61,6 +65,39 @@ "vitest": "^4.1.0" } }, + "node_modules/@ai-sdk/anthropic": { + "version": "3.0.66", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.66.tgz", + "integrity": "sha512-yJpQ2x6ACwbXo5D6HsVWd2FFnnWcetfGx4oxkG66P8FawusvrY2vL2qMiiNTruWrxEYDy+YHc3ctv8C769MMJA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.22" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.22.tgz", + "integrity": "sha512-B2OTFcRw/Pdka9ZTjpXv6T6qZ6RruRuLokyb8HwW+aoW9ndJ3YasA3/mVswyJw7VMBF8ofXgqvcrCt9KYvFifg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/gateway": { "version": "3.0.85", "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.85.tgz", @@ -138,27 +175,24 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.77", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.77.tgz", - "integrity": "sha512-t+R1BW3ahCFMNM7/8WJq7+Gw9KPA9Cl7UUK8fWPokJZ75cf/xwEd9MqB+MVNoQT45dJiom/wxybT7tqYPkCqyg==", - "license": "SEE LICENSE IN README.md", - "engines": { - "node": ">=18.0.0" + "node_modules/@anthropic-ai/sdk": { + "version": "0.82.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.82.0.tgz", + "integrity": "sha512-xdHTjL1GlUlDugHq/I47qdOKp/ROPvuHl7ROJCgUQigbvPu7asf9KcAcU1EqdrP2LuVhEKaTs7Z+ShpZDRzHdQ==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "^0.34.2", - "@img/sharp-darwin-x64": "^0.34.2", - "@img/sharp-linux-arm": "^0.34.2", - "@img/sharp-linux-arm64": "^0.34.2", - "@img/sharp-linux-x64": "^0.34.2", - "@img/sharp-linuxmusl-arm64": "^0.34.2", - "@img/sharp-linuxmusl-x64": "^0.34.2", - "@img/sharp-win32-arm64": "^0.34.2", - "@img/sharp-win32-x64": "^0.34.2" + "bin": { + "anthropic-ai-sdk": "bin/cli" }, "peerDependencies": { - "zod": "^4.0.0" + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, "node_modules/@babel/code-frame": { @@ -234,6 +268,7 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -262,6 +297,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -292,6 +328,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.5", @@ -335,6 +372,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -356,6 +394,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", @@ -373,6 +412,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -441,6 +481,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -456,6 +497,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -471,6 +513,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -517,6 +560,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -536,6 +580,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -551,6 +596,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -657,6 +711,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz", "integrity": "sha512-Qg+meC+XFxliuVSDlEPkKnaUjdaJKK6FNx/Wwl2UxhQR8pyPIuLhMavsF7ePdB9qFZUWV1jEK3ckbJir/WmF4w==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "commander": "^11.1.0", @@ -680,6 +735,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -689,6 +745,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -712,6 +769,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -724,6 +782,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -733,6 +792,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -745,6 +805,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -754,6 +815,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -766,6 +828,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -781,12 +844,14 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -796,6 +861,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -818,6 +884,7 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "dev": true, "license": "MIT", "engines": { "bun": ">=1", @@ -828,6 +895,16 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -1756,6 +1833,18 @@ "mlly": "^1.8.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", @@ -1763,11 +1852,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -1785,11 +1876,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -1807,11 +1900,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1823,11 +1918,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1839,11 +1936,16 @@ "cpu": [ "arm" ], + "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1855,11 +1957,79 @@ "cpu": [ "arm64" ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1871,11 +2041,16 @@ "cpu": [ "x64" ], + "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1887,11 +2062,16 @@ "cpu": [ "arm64" ], + "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1903,11 +2083,16 @@ "cpu": [ "x64" ], + "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -1919,11 +2104,16 @@ "cpu": [ "arm" ], + "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -1941,11 +2131,16 @@ "cpu": [ "arm64" ], + "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -1956,6 +2151,87 @@ "@img/sharp-libvips-linux-arm64": "1.2.4" } }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, "node_modules/@img/sharp-linux-x64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", @@ -1963,11 +2239,16 @@ "cpu": [ "x64" ], + "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -1985,11 +2266,16 @@ "cpu": [ "arm64" ], + "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2007,11 +2293,16 @@ "cpu": [ "x64" ], + "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2022,6 +2313,27 @@ "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", @@ -2029,11 +2341,34 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2048,11 +2383,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2064,6 +2401,7 @@ "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" @@ -2073,6 +2411,7 @@ "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", @@ -2094,6 +2433,7 @@ "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", @@ -2121,6 +2461,7 @@ "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", @@ -2135,6 +2476,7 @@ "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" @@ -2144,6 +2486,7 @@ "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" @@ -2251,49 +2594,215 @@ } } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@mswjs/interceptors": { + "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", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@next/env": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", + "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", + "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", + "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", + "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", + "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", + "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", + "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", + "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/@mswjs/interceptors": { - "version": "0.41.3", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", - "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", + "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -2306,6 +2815,7 @@ "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -2321,6 +2831,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -2333,6 +2844,7 @@ "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", @@ -2346,6 +2858,7 @@ "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" @@ -2355,6 +2868,7 @@ "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", @@ -2368,12 +2882,14 @@ "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", @@ -2384,6 +2900,7 @@ "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": { @@ -5016,6 +5533,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, "license": "MIT" }, "node_modules/@shikijs/core": { @@ -5122,6 +5640,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -5265,6 +5784,17 @@ "react": "^18.0.0 || ^19.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", @@ -5646,10 +6176,60 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, "license": "MIT", "dependencies": { "fast-glob": "^3.3.3", @@ -5657,6 +6237,14 @@ "path-browserify": "^1.0.1" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -6175,6 +6763,7 @@ "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": { @@ -6218,8 +6807,26 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -6417,11 +7024,53 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14" } }, + "node_modules/agent-tail": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/agent-tail/-/agent-tail-0.4.0.tgz", + "integrity": "sha512-El43u8Sde6UyUfqxPFIU44FS36LvfHELVQM+5XTdgCTAs8IzwHWg5erE+Qwn+HNz51BoqHH/z+T5LLYF8z2weg==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-tail-core": "^0.4.0", + "next-plugin-agent-tail": "^0.4.0", + "vite-plugin-agent-tail": "^0.4.0" + }, + "bin": { + "agent-tail": "dist/cli.mjs" + }, + "peerDependencies": { + "next": ">=13.0.0", + "react": ">=18.0.0", + "vite": ">=5.0.0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/agent-tail-core": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/agent-tail-core/-/agent-tail-core-0.4.0.tgz", + "integrity": "sha512-eSpGcuTiMbqpuq9N7ruztS3DsdxR/xEPYRw/D8MVfVLfMEYifaGuzqzxtzhRpc1piONTLahLidkp39GorbF1jg==", + "dev": true, + "license": "MIT", + "bin": { + "agent-tail": "dist/cli.mjs" + } + }, "node_modules/ai": { "version": "6.0.143", "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.143.tgz", @@ -6483,6 +7132,7 @@ "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" @@ -6492,6 +7142,7 @@ "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" @@ -6507,6 +7158,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -6521,6 +7173,17 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -6542,6 +7205,7 @@ "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -6571,6 +7235,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -6670,6 +7335,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -6682,6 +7348,7 @@ "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" @@ -6758,6 +7425,7 @@ "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" @@ -6811,6 +7479,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6967,6 +7636,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" @@ -6982,6 +7652,7 @@ "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6994,15 +7665,25 @@ "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" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/cliui": { "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", @@ -7042,12 +7723,14 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, "license": "MIT" }, "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" @@ -7060,6 +7743,7 @@ "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": { @@ -7089,6 +7773,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -7240,6 +7925,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.1", @@ -7280,6 +7966,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -7819,6 +8506,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -7879,6 +8567,7 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -7902,6 +8591,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7911,6 +8601,7 @@ "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", @@ -7927,6 +8618,7 @@ "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" @@ -7939,6 +8631,7 @@ "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" @@ -8027,11 +8720,20 @@ "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dompurify": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", @@ -8045,6 +8747,7 @@ "version": "17.4.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -8696,6 +9399,7 @@ "version": "0.4.18", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "dev": true, "license": "MIT", "dependencies": { "@ecies/ciphers": "^0.2.5", @@ -8725,6 +9429,7 @@ "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": { @@ -8774,6 +9479,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8783,6 +9489,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -8913,6 +9620,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -8966,6 +9674,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -9084,6 +9793,7 @@ "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", @@ -9123,6 +9833,7 @@ "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" @@ -9149,6 +9860,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, "funding": [ { "type": "github", @@ -9172,6 +9884,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" @@ -9193,6 +9906,7 @@ "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" @@ -9266,6 +9980,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -9347,6 +10062,7 @@ "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -9384,6 +10100,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, "license": "MIT" }, "node_modules/gensync": { @@ -9399,6 +10116,7 @@ "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.*" @@ -9453,6 +10171,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "dev": true, "license": "MIT", "engines": { "node": ">=14.16" @@ -9478,6 +10197,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, "license": "MIT", "dependencies": { "@sec-ant/readable-stream": "^0.4.1", @@ -9513,6 +10233,7 @@ "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" @@ -9543,6 +10264,7 @@ "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" @@ -9554,6 +10276,37 @@ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", "license": "MIT" }, + "node_modules/happy-dom": { + "version": "20.8.9", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz", + "integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9858,6 +10611,7 @@ "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/hono": { @@ -9913,6 +10667,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -9926,6 +10681,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -9971,6 +10727,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -9980,6 +10737,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -10065,6 +10823,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-decimal": { @@ -10081,6 +10840,7 @@ "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" @@ -10096,6 +10856,7 @@ "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" @@ -10105,6 +10866,7 @@ "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" @@ -10114,6 +10876,7 @@ "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" @@ -10136,6 +10899,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -10148,6 +10912,7 @@ "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" @@ -10166,6 +10931,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10178,12 +10944,14 @@ "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" @@ -10193,6 +10961,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10223,6 +10992,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10235,6 +11005,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10247,6 +11018,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10259,6 +11031,7 @@ "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" @@ -10313,6 +11086,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10337,6 +11111,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema": { @@ -10345,6 +11120,19 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-typed": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", @@ -10367,6 +11155,7 @@ "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" @@ -10409,6 +11198,7 @@ "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" @@ -10702,6 +11492,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/lodash-es": { @@ -10714,6 +11505,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -10730,6 +11522,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -10742,6 +11535,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10778,6 +11572,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -11141,12 +11946,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, "license": "MIT" }, "node_modules/merge2": { "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" @@ -11848,6 +12655,7 @@ "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", @@ -11861,6 +12669,7 @@ "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" @@ -11911,6 +12720,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11920,6 +12730,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -11944,6 +12755,7 @@ "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -12033,6 +12845,7 @@ "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": { @@ -12077,6 +12890,7 @@ "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" @@ -12090,12 +12904,14 @@ "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" @@ -12134,6 +12950,124 @@ "node": ">= 0.6" } }, + "node_modules/next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", + "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@next/env": "16.2.2", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.2", + "@next/swc-darwin-x64": "16.2.2", + "@next/swc-linux-arm64-gnu": "16.2.2", + "@next/swc-linux-arm64-musl": "16.2.2", + "@next/swc-linux-x64-gnu": "16.2.2", + "@next/swc-linux-x64-musl": "16.2.2", + "@next/swc-win32-arm64-msvc": "16.2.2", + "@next/swc-win32-x64-msvc": "16.2.2", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-plugin-agent-tail": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/next-plugin-agent-tail/-/next-plugin-agent-tail-0.4.0.tgz", + "integrity": "sha512-c6tkPCF23WTS0Y9CpbOauqEiNbzJG7g292InIUlnxBW0C5z7iYnmX39UMVikMMPbh/YKPzdsCOugeWpryJ3U5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-tail-core": "^0.4.0" + }, + "peerDependencies": { + "next": ">=13.0.0" + } + }, + "node_modules/next/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/node-abi": { "version": "3.89.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", @@ -12163,6 +13097,7 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", + "dev": true, "funding": [ { "type": "github", @@ -12182,6 +13117,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -12206,6 +13142,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^4.0.0", @@ -12222,6 +13159,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12255,6 +13193,7 @@ "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -12296,6 +13235,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" @@ -12328,6 +13268,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.4.0", @@ -12348,6 +13289,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -12371,6 +13313,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12383,6 +13326,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -12395,12 +13339,14 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, "license": "MIT" }, "node_modules/ora/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==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -12418,6 +13364,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -12433,6 +13380,7 @@ "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": { @@ -12548,6 +13496,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -12585,6 +13534,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -12603,6 +13553,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12636,6 +13587,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, "license": "MIT" }, "node_modules/path-data-parser": { @@ -12755,6 +13707,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -12786,6 +13739,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -12821,10 +13775,41 @@ "node": ">=10" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" @@ -12840,6 +13825,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -12853,6 +13839,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12910,6 +13897,7 @@ "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", @@ -13072,6 +14060,14 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -13168,6 +14164,7 @@ "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, "license": "MIT", "dependencies": { "ast-types": "^0.16.1", @@ -13395,6 +14392,7 @@ "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" @@ -13413,6 +14411,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -13432,6 +14431,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, "license": "MIT", "dependencies": { "onetime": "^7.0.0", @@ -13448,12 +14448,14 @@ "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", @@ -13542,6 +14544,7 @@ "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" @@ -13554,6 +14557,7 @@ "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", @@ -13706,6 +14710,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.1.2.tgz", "integrity": "sha512-qNQcCavkbYsgBj+X09tF2bTcwRd8abR880bsFkDU2kMqceMCLAm5c+cLg7kWDhfh1H9g08knpQ5ZEf6y/co16g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -13751,11 +14756,74 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13892,6 +14960,7 @@ "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" @@ -13949,12 +15018,14 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -14017,6 +15088,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14069,6 +15141,7 @@ "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": { @@ -14084,6 +15157,7 @@ "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", @@ -14112,6 +15186,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-keys": "^1.0.0", @@ -14129,6 +15204,7 @@ "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" @@ -14141,6 +15217,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -14150,6 +15227,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14176,6 +15254,31 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -14248,6 +15351,7 @@ "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" @@ -14329,6 +15433,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, "license": "MIT" }, "node_modules/tinybench": { @@ -14387,6 +15492,7 @@ "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" @@ -14399,12 +15505,14 @@ "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" @@ -14426,6 +15534,7 @@ "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" @@ -14464,6 +15573,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -14477,6 +15592,7 @@ "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dev": true, "license": "MIT", "dependencies": { "@ts-morph/common": "~0.27.0", @@ -14487,6 +15603,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, "license": "MIT", "dependencies": { "json5": "^2.2.2", @@ -14548,6 +15665,7 @@ "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" @@ -14577,7 +15695,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14604,6 +15722,7 @@ "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" @@ -14731,6 +15850,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -14749,6 +15869,7 @@ "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" @@ -14868,6 +15989,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -14998,6 +16120,19 @@ } } }, + "node_modules/vite-plugin-agent-tail": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-agent-tail/-/vite-plugin-agent-tail-0.4.0.tgz", + "integrity": "sha512-5MpQUh9D+Jp0egUIn+g9zWorBFGpXLRWfbYNptF8lgneVsAmZUglyvic8ff7XVlSIywIiOT0x+GByqotwPKKkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-tail-core": "^0.4.0" + }, + "peerDependencies": { + "vite": ">=5.0.0" + } + }, "node_modules/vitest": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", @@ -15143,11 +16278,22 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -15184,6 +16330,7 @@ "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", @@ -15203,10 +16350,33 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, "license": "MIT", "dependencies": { "is-wsl": "^3.1.0", @@ -15223,6 +16393,7 @@ "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" @@ -15238,6 +16409,7 @@ "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", @@ -15256,6 +16428,7 @@ "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" @@ -15265,6 +16438,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -15277,6 +16451,7 @@ "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 43081e2d..a7e774a6 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,21 @@ "scripts": { "build": "vite build", "check": "npm run fmt:check && npm run lint", - "dev": "concurrently \"vite\" \"npx tsx --env-file=.env --watch src/server/index.ts\"", + "dev": "agent-tail run 'vite: lsof -ti:5173 | xargs kill -9 2>/dev/null; vite' 'api: lsof -ti:3000 | xargs kill -9 2>/dev/null; npx tsx --env-file=.env --watch src/server/index.ts'", "fix": "npm run lint:fix && npm run fmt", "fmt": "oxfmt src/", "fmt:check": "oxfmt --check src/", "lint": "oxlint --type-aware --type-check src/", "lint:fix": "oxlint --type-aware --type-check --fix src/", "server": "npx tsx --env-file=.env src/server/index.ts", + "studio": "drizzle-kit studio", "test": "vitest run", "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/claude-agent-sdk": "^0.2.77", + "@anthropic-ai/sdk": "^0.82.0", "@fontsource-variable/geist": "^5.2.8", "@modelcontextprotocol/sdk": "^1.27.1", "@radix-ui/react-use-controllable-state": "^1.2.2", @@ -53,7 +55,6 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "shadcn": "^4.1.2", "shiki": "^4.0.2", "streamdown": "^2.5.0", "tailwind-merge": "^3.5.0", @@ -63,16 +64,20 @@ "zod": "^4.3.6" }, "devDependencies": { + "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", "@types/express": "^5.0.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/supertest": "^7.2.0", + "agent-tail": "^0.4.0", "concurrently": "^9.2.1", "drizzle-kit": "^0.31.10", + "happy-dom": "^20.8.9", "oxfmt": "^0.43.0", "oxlint": "^1.58.0", "oxlint-tsgolint": "^0.19.0", + "shadcn": "^4.1.2", "supertest": "^7.2.2", "tsx": "^4.21.0", "typescript": "^5.9.3", diff --git a/spike/filesystem-tools.ts b/spike/filesystem-tools.ts new file mode 100644 index 00000000..d54d2f1c --- /dev/null +++ b/spike/filesystem-tools.ts @@ -0,0 +1,87 @@ +/** + * Spike: Core filesystem tools with ToolLoopAgent + * + * Question: Can a ToolLoopAgent with generic filesystem tools reliably + * explore and characterize an existing project? + * + * Run: npx tsx --env-file=.env spike/filesystem-tools.ts [target-dir] + * + * Defaults to the brunch project root if no target dir is given. + */ +import { resolve } from 'node:path'; + +import { anthropic } from '@ai-sdk/anthropic'; +import { ToolLoopAgent, stepCountIs } from 'ai'; + +import { createCoreTools } from '../src/server/tools/index.js'; + +const targetDir = resolve(process.argv[2] ?? '.'); + +console.log(`\n═══ Filesystem Tools Spike ═══`); +console.log(`Target: ${targetDir}`); +console.log(`Model: ${process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514'}\n`); + +const tools = createCoreTools(targetDir); + +const agent = new ToolLoopAgent({ + model: anthropic(process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514'), + instructions: `You are a project analyst. You have access to filesystem tools to explore a codebase. + +Your job is to explore the project at the working directory and produce a structured characterization. + +Strategy: +1. Start with list_directory to see the top-level structure +2. Read key files (package.json, README, config files) to understand the tech stack +3. Use find_files and grep to understand the architecture +4. Read a few key source files to understand the domain + +Produce a summary covering: +- Project name and purpose +- Tech stack (languages, frameworks, key dependencies) +- Architecture (main modules, entry points, data flow) +- Key abstractions and domain concepts +- Current state (what's built, what's in progress)`, + tools, + providerOptions: { + anthropic: { + sendReasoning: true, + thinking: { + type: 'enabled', + budgetTokens: 8000, + }, + }, + }, + maxOutputTokens: 8000, + stopWhen: stepCountIs(30), +}); + +async function run() { + const startMs = Date.now(); + let stepCount = 0; + + const result = await agent.generate({ + prompt: `Explore and characterize the project in the current working directory. Use the available tools to understand its structure, purpose, and current state.`, + onStepFinish: (step) => { + stepCount++; + const toolCalls = step.toolCalls?.length ?? 0; + const toolNames = step.toolCalls?.map((tc) => tc.toolName).join(', ') ?? 'none'; + console.log(` Step ${stepCount}: ${toolCalls} tool call(s) [${toolNames}]`); + }, + }); + + const durationMs = Date.now() - startMs; + + console.log(`\n═══ Results ═══`); + console.log(`Steps: ${stepCount}`); + console.log(`Duration: ${(durationMs / 1000).toFixed(1)}s`); + const u = result.usage; + console.log(`Tokens: ${u.totalTokens} (${u.promptTokens} prompt, ${u.completionTokens} completion)`); + console.log(`Finish reason: ${result.finishReason}`); + console.log(`\n═══ Agent Summary ═══\n`); + console.log(result.text); +} + +run().catch((err) => { + console.error('Spike failed:', err); + process.exit(1); +}); diff --git a/spike/raw-sdk-tool-use.ts b/spike/raw-sdk-tool-use.ts new file mode 100644 index 00000000..6fbd12d0 --- /dev/null +++ b/spike/raw-sdk-tool-use.ts @@ -0,0 +1,263 @@ +/** + * Spike: Raw Anthropic SDK tool execution + * + * Question: Can we replace @anthropic-ai/claude-agent-sdk query() with + * @anthropic-ai/sdk client.messages.stream() for reliable tool calls? + * + * Run: npx tsx --env-file=.env spike/raw-sdk-tool-use.ts + */ +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic(); + +// Hand-written tool schema (oracle advice: avoid Zod-to-JSON-Schema edge cases for spike) +const ASK_QUESTION_TOOL: Anthropic.Messages.Tool = { + name: 'ask_question', + description: + 'Ask the user a structured interview question with options, strategic grounding, and impact signal.', + input_schema: { + type: 'object' as const, + properties: { + question: { type: 'string', description: 'The interview question' }, + why: { type: 'string', description: 'Why this question matters for the spec' }, + impact: { type: 'string', enum: ['high', 'medium', 'low'] }, + options: { + type: 'array', + items: { + type: 'object', + properties: { + content: { type: 'string' }, + is_recommended: { type: 'boolean' }, + }, + required: ['content', 'is_recommended'], + }, + minItems: 2, + }, + }, + required: ['question', 'why', 'impact', 'options'], + }, +}; + +// ── Test 1: Forced tool call with streaming ────────────────────────── + +async function testForcedToolCall() { + console.log('\n═══ Test 1: Forced tool_choice with streaming ═══\n'); + + const startMs = Date.now(); + + const stream = client.messages.stream({ + model: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514', + max_tokens: 1024, + system: `You are a spec elicitation interviewer conducting the SCOPE phase. +Your job is to understand the user's project goal through structured questions. +For every turn, you MUST use the ask_question tool. Never respond with plain text.`, + messages: [{ role: 'user', content: 'I want to build a local-first note-taking app' }], + tools: [ASK_QUESTION_TOOL], + tool_choice: { type: 'tool' as const, name: 'ask_question' }, + }); + + // Collect streaming events to verify format + const eventTypes: string[] = []; + let toolCallId = ''; + let toolName = ''; + let jsonChunks = ''; + + stream.on('message_start', () => eventTypes.push('message_start')); + stream.on('contentBlockStart', (block) => { + eventTypes.push(`content_block_start:${block.content_block.type}`); + if (block.content_block.type === 'tool_use') { + toolCallId = block.content_block.id; + toolName = block.content_block.name; + } + }); + stream.on('inputJson', (_delta, snapshot) => { + jsonChunks = snapshot; + }); + stream.on('contentBlockStop', () => eventTypes.push('content_block_stop')); + stream.on('message_stop', () => eventTypes.push('message_stop')); + + const finalMessage = await stream.finalMessage(); + const durationMs = Date.now() - startMs; + + // Extract results + const toolUse = finalMessage.content.find( + (block): block is Anthropic.Messages.ToolUseBlock => + block.type === 'tool_use' && block.name === 'ask_question', + ); + + console.log('Event types observed:', eventTypes); + console.log('Stop reason:', finalMessage.stop_reason); + console.log('Tool call ID:', toolCallId); + console.log('Tool name:', toolName); + console.log('Tool use block found:', !!toolUse); + console.log('Duration:', durationMs, 'ms'); + console.log( + 'Usage:', + `in=${finalMessage.usage.input_tokens} out=${finalMessage.usage.output_tokens}`, + ); + + if (toolUse) { + const args = toolUse.input as Record; + console.log('\n── Tool call args ──'); + console.log(' question:', args.question); + console.log(' why:', args.why); + console.log(' impact:', args.impact); + console.log(' options:', JSON.stringify(args.options, null, 2)); + console.log('\n✅ PASS: Model called ask_question with structured args'); + } else { + console.log('\n❌ FAIL: Model did not call ask_question'); + } + + return !!toolUse; +} + +// ── Test 2: Raw stream events match translator expectations ────────── + +async function testRawStreamEvents() { + console.log('\n═══ Test 2: Raw stream event format (for translator compat) ═══\n'); + + // Use the low-level streaming API to get raw SSE events + const rawStream = await client.messages.create({ + model: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514', + max_tokens: 1024, + stream: true, + system: 'Ask one structured question using the ask_question tool.', + messages: [{ role: 'user', content: 'I want to build a CLI password manager' }], + tools: [ASK_QUESTION_TOOL], + tool_choice: { type: 'tool' as const, name: 'ask_question' }, + }); + + // Collect raw events to check shape + const rawEvents: Array<{ type: string; hasIndex: boolean; hasContentBlock: boolean; hasDelta: boolean }> = []; + + for await (const event of rawStream) { + rawEvents.push({ + type: event.type, + hasIndex: 'index' in event, + hasContentBlock: 'content_block' in event, + hasDelta: 'delta' in event, + }); + } + + console.log('Raw event shapes:'); + for (const e of rawEvents) { + console.log(` ${e.type} | index: ${e.hasIndex} | content_block: ${e.hasContentBlock} | delta: ${e.hasDelta}`); + } + + // Check: events are directly typed (NOT wrapped in { type: 'stream_event', event: {...} }) + const hasMessageStart = rawEvents.some((e) => e.type === 'message_start'); + const hasContentBlockStart = rawEvents.some((e) => e.type === 'content_block_start'); + const hasContentBlockDelta = rawEvents.some((e) => e.type === 'content_block_delta'); + const hasContentBlockStop = rawEvents.some((e) => e.type === 'content_block_stop'); + const hasMessageDelta = rawEvents.some((e) => e.type === 'message_delta'); + const hasMessageStop = rawEvents.some((e) => e.type === 'message_stop'); + + console.log('\nEvent coverage:'); + console.log(' message_start:', hasMessageStart); + console.log(' content_block_start:', hasContentBlockStart); + console.log(' content_block_delta:', hasContentBlockDelta); + console.log(' content_block_stop:', hasContentBlockStop); + console.log(' message_delta:', hasMessageDelta); + console.log(' message_stop:', hasMessageStop); + + const allPresent = + hasMessageStart && + hasContentBlockStart && + hasContentBlockDelta && + hasContentBlockStop && + hasMessageStop; + + if (allPresent) { + console.log( + '\n✅ PASS: All expected event types present. Translator needs: remove stream_event envelope, consume events directly', + ); + } else { + console.log('\n❌ FAIL: Missing expected event types'); + } + + return allPresent; +} + +// ── Test 3: Observer structured output ─────────────────────────────── + +async function testObserverStructuredOutput() { + console.log('\n═══ Test 3: Observer structured output via raw API ═══\n'); + + // Test if we can get structured JSON output without the Agent SDK + const response = await client.messages.create({ + model: process.env.OBSERVER_MODEL || 'claude-haiku-4-5-20251001', + max_tokens: 2048, + system: `Extract decisions and assumptions from this interview exchange. +Return JSON matching the schema exactly.`, + messages: [ + { + role: 'user', + content: `Q: What platform are you targeting? +A: Desktop only, macOS and Linux. No mobile. + +Q: What database should we use? +A: SQLite — I want it to be local-first with no server. + +Existing entities: (none) + +Extract any NEW decisions and assumptions from these exchanges.`, + }, + ], + }); + + const textBlock = response.content.find( + (b): b is Anthropic.Messages.TextBlock => b.type === 'text', + ); + + console.log('Stop reason:', response.stop_reason); + console.log('Has text block:', !!textBlock); + console.log( + 'Usage:', + `in=${response.usage.input_tokens} out=${response.usage.output_tokens}`, + ); + + if (textBlock) { + console.log('\nRaw response (first 500 chars):', textBlock.text.slice(0, 500)); + + // Try to parse as JSON + try { + // Strip markdown code fences if present + const jsonStr = textBlock.text.replace(/^```json\n?/, '').replace(/\n?```$/, ''); + const parsed = JSON.parse(jsonStr); + console.log('\n✅ PASS: Response parses as valid JSON'); + console.log(' decisions count:', parsed.decisions?.length ?? 'missing'); + console.log(' assumptions count:', parsed.assumptions?.length ?? 'missing'); + } catch { + console.log('\n⚠️ WARNING: Response is not valid JSON — may need output_config.format'); + } + } + + return !!textBlock; +} + +// ── Run all tests ──────────────────────────────────────────────────── + +async function main() { + console.log('Spike: Raw Anthropic SDK Tool Use'); + console.log('=================================\n'); + + const results = { + forcedToolCall: await testForcedToolCall(), + rawStreamEvents: await testRawStreamEvents(), + observerOutput: await testObserverStructuredOutput(), + }; + + console.log('\n═══ Summary ═══'); + console.log('Forced tool call:', results.forcedToolCall ? '✅' : '❌'); + console.log('Stream events:', results.rawStreamEvents ? '✅' : '❌'); + console.log('Observer output:', results.observerOutput ? '✅' : '❌'); + + const allPass = Object.values(results).every(Boolean); + console.log('\nOverall:', allPass ? '✅ ALL PASS' : '❌ SOME FAILED'); + process.exit(allPass ? 0 : 1); +} + +main().catch((err) => { + console.error('Spike failed:', err); + process.exit(1); +}); diff --git a/src/client/build-boundary.test.ts b/src/client/build-boundary.test.ts new file mode 100644 index 00000000..e4d0c78f --- /dev/null +++ b/src/client/build-boundary.test.ts @@ -0,0 +1,98 @@ +// @vitest-environment node + +import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { build } from 'vite'; +import { afterEach, describe, expect, it } from 'vitest'; + +describe('client build boundary', () => { + const tempDirs: string[] = []; + + afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { force: true, recursive: true }); + } + }); + + const buildClient = async ({ minify }: { minify: boolean }) => { + const outDir = mkdtempSync(join(tmpdir(), 'brunch-client-build-')); + tempDirs.push(outDir); + + await build({ + build: { + manifest: true, + minify, + outDir, + }, + configFile: 'vite.config.ts', + logLevel: 'silent', + }); + + const manifestPath = join(outDir, '.vite', 'manifest.json'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record< + string, + { file?: string; isEntry?: boolean } + >; + const entry = manifest['index.html']; + + expect(entry?.isEntry).toBe(true); + expect(entry?.file).toBeTruthy(); + + return { + entryFile: readFileSync(join(outDir, entry.file!), 'utf8'), + entryPath: join(outDir, entry.file!), + manifest, + outDir, + entry, + }; + }; + + it('keeps debug and rich markdown rendering out of the default client entrypoint', async () => { + const readableBuild = await buildClient({ minify: false }); + + expect(readableBuild.entryFile).toContain('/debug'); + expect(readableBuild.entryFile).toContain('/project/$id'); + expect(readableBuild.entryFile).not.toContain('Component Debug'); + expect(readableBuild.entryFile).not.toContain('outer-loop testing'); + expect(readableBuild.entryFile).not.toContain('streamdown'); + expect(readableBuild.entryFile).not.toContain('createHighlighter'); + + const richRenderingChunk = Object.values(readableBuild.manifest).find((chunk) => { + if (!chunk.file || chunk.file === readableBuild.entry.file) { + return false; + } + + const chunkSource = readFileSync(join(readableBuild.outDir, chunk.file), 'utf8'); + return chunkSource.includes('streamdown'); + }); + + expect(richRenderingChunk?.file).toBeTruthy(); + + const highlighterChunk = Object.values(readableBuild.manifest).find((chunk) => { + if (!chunk.file || chunk.file === readableBuild.entry.file) { + return false; + } + + const chunkSource = readFileSync(join(readableBuild.outDir, chunk.file), 'utf8'); + return chunkSource.includes('createHighlighter'); + }); + + expect(highlighterChunk?.file).toBeTruthy(); + + const debugChunk = Object.values(readableBuild.manifest).find((chunk) => { + if (!chunk.file || chunk.file === readableBuild.entry.file) { + return false; + } + + const chunkSource = readFileSync(join(readableBuild.outDir, chunk.file), 'utf8'); + return chunkSource.includes('Component Debug'); + }); + + expect(debugChunk?.file).toBeTruthy(); + + const minifiedBuild = await buildClient({ minify: true }); + expect(statSync(minifiedBuild.entryPath).size).toBeLessThan(950_000); + }, 30_000); +}); diff --git a/src/client/capabilities/code-highlighting.ts b/src/client/capabilities/code-highlighting.ts new file mode 100644 index 00000000..d3024524 --- /dev/null +++ b/src/client/capabilities/code-highlighting.ts @@ -0,0 +1,84 @@ +'use client'; + +import type { BundledLanguage, ThemedToken } from 'shiki'; + +export type CodeLanguage = BundledLanguage; +export type CodeToken = ThemedToken; + +export interface TokenizedCode { + tokens: CodeToken[][]; + fg: string; + bg: string; +} + +const tokensCache = new Map(); +const inFlightHighlights = new Map>(); +let richCodeHighlighterPromise: Promise | null = + null; + +const getTokensCacheKey = (code: string, language: CodeLanguage) => { + const start = code.slice(0, 100); + const end = code.length > 100 ? code.slice(-100) : ''; + return `${language}:${code.length}:${start}:${end}`; +}; + +const loadRichCodeHighlighter = () => { + if (!richCodeHighlighterPromise) { + richCodeHighlighterPromise = import('./rich-code-highlighting.js').then( + (module) => module.highlightCodeRich, + ); + } + + return richCodeHighlighterPromise; +}; + +export const preloadRichCodeHighlighter = () => loadRichCodeHighlighter(); + +export const createPlainCodeTokens = (code: string): TokenizedCode => ({ + bg: 'transparent', + fg: 'inherit', + tokens: code.split('\n').map((line) => + line === '' + ? [] + : [ + { + color: 'inherit', + content: line, + } as CodeToken, + ], + ), +}); + +export const getCachedHighlightedCode = (code: string, language: CodeLanguage): TokenizedCode | null => { + const tokensCacheKey = getTokensCacheKey(code, language); + return tokensCache.get(tokensCacheKey) ?? null; +}; + +export const highlightCode = async (code: string, language: CodeLanguage): Promise => { + const tokensCacheKey = getTokensCacheKey(code, language); + const cached = tokensCache.get(tokensCacheKey); + + if (cached) { + return cached; + } + + const existingRequest = inFlightHighlights.get(tokensCacheKey); + if (existingRequest) { + return existingRequest; + } + + const request = loadRichCodeHighlighter() + .then((highlightCodeRich) => highlightCodeRich(code, language)) + .then((tokenized) => { + tokensCache.set(tokensCacheKey, tokenized); + inFlightHighlights.delete(tokensCacheKey); + return tokenized; + }) + .catch((error) => { + inFlightHighlights.delete(tokensCacheKey); + throw error; + }); + + inFlightHighlights.set(tokensCacheKey, request); + return request; +}; diff --git a/src/client/capabilities/markdown-rendering.test.tsx b/src/client/capabilities/markdown-rendering.test.tsx new file mode 100644 index 00000000..faba67f9 --- /dev/null +++ b/src/client/capabilities/markdown-rendering.test.tsx @@ -0,0 +1,68 @@ +// @vitest-environment happy-dom + +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@streamdown/cjk', () => ({ cjk: {} })); +vi.mock('@streamdown/code', () => ({ code: {} })); +vi.mock('@streamdown/math', () => ({ math: {} })); +vi.mock('@streamdown/mermaid', () => ({ mermaid: {} })); +vi.mock('streamdown', () => ({ + Streamdown: ({ children, ...props }: React.HTMLAttributes) => ( +
{children}
+ ), +})); + +describe('MarkdownRenderer', () => { + it('keeps plain text on the immediate rendering path', async () => { + const { MarkdownRenderer, needsRichMarkdownRendering } = await import('./markdown-rendering.js'); + + expect(needsRichMarkdownRendering('Just a plain answer.')).toBe(false); + + render(Just a plain answer.); + + expect(screen.getByText('Just a plain answer.').closest('[data-rendering-mode="plain"]')).toBeTruthy(); + + await Promise.resolve(); + + expect(screen.queryByText('Just a plain answer.')?.closest('[data-rendering-mode="rich"]')).toBeNull(); + }); + + it('renders fenced code as plain text first, then upgrades to rich rendering', async () => { + const { MarkdownRenderer, needsRichMarkdownRendering } = await import('./markdown-rendering.js'); + const content = '```typescript\nconst answer = 42\n```'; + + expect(needsRichMarkdownRendering(content)).toBe(true); + expect(needsRichMarkdownRendering('```mermaid\ngraph TD\nA-->B\n```')).toBe(true); + + render({content}); + + expect(screen.getByText(/const answer = 42/).closest('[data-rendering-mode="plain"]')).toBeTruthy(); + + await waitFor(() => { + expect(screen.getByText(/const answer = 42/).closest('[data-rendering-mode="rich"]')).toBeTruthy(); + }); + }); + + it('keeps rich markdown on the plain first-paint path while the message is animating', async () => { + const { MarkdownRenderer } = await import('./markdown-rendering.js'); + const content = '```typescript\nconst answer = 42\n```'; + + const { container, rerender } = render({content}); + + expect(container.querySelector('[data-rendering-mode="plain"]')?.textContent).toContain( + 'const answer = 42', + ); + + await Promise.resolve(); + expect(container.querySelector('[data-rendering-mode="rich"]')).toBeNull(); + + rerender({content}); + + await waitFor(() => { + expect(container.querySelector('[data-rendering-mode="rich"]')?.textContent).toContain( + 'const answer = 42', + ); + }); + }); +}); diff --git a/src/client/capabilities/markdown-rendering.tsx b/src/client/capabilities/markdown-rendering.tsx new file mode 100644 index 00000000..3cd4050c --- /dev/null +++ b/src/client/capabilities/markdown-rendering.tsx @@ -0,0 +1,108 @@ +'use client'; + +import type { ComponentType } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { preloadRichCodeHighlighter } from '@/capabilities/code-highlighting'; +import { cn } from '@/lib/utils'; + +export interface MarkdownRendererProps { + children?: string; + className?: string; + isAnimating?: boolean; +} + +const MARKDOWN_ENHANCEMENT_PATTERNS = [ + /```[\s\S]+?```/m, + /`[^`\n]+`/, + /(^|\n)#{1,6}\s/m, + /(^|\n)\s*[-*+]\s/m, + /(^|\n)\s*\d+\.\s/m, + /(^|\n)>\s/m, + /!\[[^\]]*\]\([^)]+\)/, + /\[[^\]]+\]\([^)]+\)/, + /\*\*[^*]+\*|__[^_]+__/, + /\$\$[\s\S]+?\$\$|(^|[^$])\$[^$\n]+\$(?!\$)/, + /(^|\n)\|.+\|/, +]; + +export const needsRichMarkdownRendering = (content: string) => + MARKDOWN_ENHANCEMENT_PATTERNS.some((pattern) => pattern.test(content)); + +const CODE_FENCE_PATTERN = /```[\s\S]+?```/m; + +const shouldPreloadCodeHighlighting = (content: string) => CODE_FENCE_PATTERN.test(content); + +const PlainTextRenderer = ({ + className, + children, + onIntentToEnhance, +}: MarkdownRendererProps & { onIntentToEnhance?: () => void }) => ( +
+ {children} +
+); + +let richMarkdownRendererPromise: Promise> | null = null; + +const loadRichMarkdownRenderer = () => { + if (!richMarkdownRendererPromise) { + richMarkdownRendererPromise = import('./rich-markdown-rendering.js').then( + (module) => module.RichMarkdownRenderer, + ); + } + + return richMarkdownRendererPromise; +}; + +export const preloadRichMarkdownRenderer = () => loadRichMarkdownRenderer(); + +export const MarkdownRenderer = ({ children, ...props }: MarkdownRendererProps) => { + const content = typeof children === 'string' ? children : ''; + const shouldEnhance = useMemo(() => needsRichMarkdownRendering(content), [content]); + const [RichRenderer, setRichRenderer] = useState | null>(null); + const warmEnhancementBoundary = useCallback(() => { + if (!shouldEnhance) { + return; + } + + void preloadRichMarkdownRenderer(); + if (shouldPreloadCodeHighlighting(content)) { + void preloadRichCodeHighlighter(); + } + }, [content, shouldEnhance]); + + useEffect(() => { + if (!shouldEnhance || props.isAnimating) { + return; + } + + let cancelled = false; + + void loadRichMarkdownRenderer().then((renderer) => { + if (!cancelled) { + setRichRenderer(() => renderer); + } + }); + + return () => { + cancelled = true; + }; + }, [props.isAnimating, shouldEnhance]); + + if (!shouldEnhance || !RichRenderer) { + return ( + + {content} + + ); + } + + return {content}; +}; diff --git a/src/client/capabilities/reasoning-rendering.tsx b/src/client/capabilities/reasoning-rendering.tsx new file mode 100644 index 00000000..68fcb299 --- /dev/null +++ b/src/client/capabilities/reasoning-rendering.tsx @@ -0,0 +1,9 @@ +'use client'; + +import type { ComponentProps } from 'react'; + +import { MarkdownRenderer } from './markdown-rendering'; + +export type ReasoningRendererProps = ComponentProps; + +export const ReasoningRenderer = (props: ReasoningRendererProps) => ; diff --git a/src/client/capabilities/rich-code-highlighting.ts b/src/client/capabilities/rich-code-highlighting.ts new file mode 100644 index 00000000..c030b0f6 --- /dev/null +++ b/src/client/capabilities/rich-code-highlighting.ts @@ -0,0 +1,43 @@ +'use client'; + +import type { BundledTheme, HighlighterGeneric } from 'shiki'; +import { createHighlighter } from 'shiki'; + +import type { CodeLanguage, TokenizedCode } from './code-highlighting'; + +const highlighterCache = new Map>>(); + +const getHighlighter = (language: CodeLanguage): Promise> => { + const cached = highlighterCache.get(language); + if (cached) { + return cached; + } + + const highlighterPromise = createHighlighter({ + langs: [language], + themes: ['github-light', 'github-dark'], + }); + + highlighterCache.set(language, highlighterPromise); + return highlighterPromise; +}; + +export const highlightCodeRich = async (code: string, language: CodeLanguage): Promise => { + const highlighter = await getHighlighter(language); + const availableLangs = highlighter.getLoadedLanguages(); + const langToUse = availableLangs.includes(language) ? language : 'text'; + + const result = highlighter.codeToTokens(code, { + lang: langToUse, + themes: { + dark: 'github-dark', + light: 'github-light', + }, + }); + + return { + bg: result.bg ?? 'transparent', + fg: result.fg ?? 'inherit', + tokens: result.tokens, + }; +}; diff --git a/src/client/capabilities/rich-markdown-rendering.tsx b/src/client/capabilities/rich-markdown-rendering.tsx new file mode 100644 index 00000000..34139203 --- /dev/null +++ b/src/client/capabilities/rich-markdown-rendering.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { cjk } from '@streamdown/cjk'; +import { code } from '@streamdown/code'; +import { math } from '@streamdown/math'; +import { mermaid } from '@streamdown/mermaid'; +import { Streamdown } from 'streamdown'; + +import type { MarkdownRendererProps } from './markdown-rendering'; + +const markdownRenderingPlugins = { cjk, code, math, mermaid }; + +export const RichMarkdownRenderer = ({ children, ...props }: MarkdownRendererProps) => ( +
+ + {children} + +
+); diff --git a/src/client/capability-boundaries.test.ts b/src/client/capability-boundaries.test.ts new file mode 100644 index 00000000..71eca20c --- /dev/null +++ b/src/client/capability-boundaries.test.ts @@ -0,0 +1,54 @@ +// @vitest-environment node + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const readClientFile = (relativePath: string) => + readFileSync(join(process.cwd(), 'src/client', relativePath), 'utf8'); + +describe('client capability boundaries', () => { + it('routes streamed markdown and reasoning through a progressive enhancement boundary', () => { + const messageSource = readClientFile('components/ai-elements/message.tsx'); + const reasoningSource = readClientFile('components/ai-elements/reasoning.tsx'); + const markdownCapabilitySource = readClientFile('capabilities/markdown-rendering.tsx'); + const richMarkdownCapabilitySource = readClientFile('capabilities/rich-markdown-rendering.tsx'); + const reasoningCapabilitySource = readClientFile('capabilities/reasoning-rendering.tsx'); + + expect(markdownCapabilitySource).toContain("import('./rich-markdown-rendering.js')"); + expect(markdownCapabilitySource).toContain('export const preloadRichMarkdownRenderer'); + expect(markdownCapabilitySource).not.toContain("from 'streamdown'"); + expect(richMarkdownCapabilitySource).toContain("from 'streamdown'"); + expect(richMarkdownCapabilitySource).toContain("from '@streamdown/mermaid'"); + expect(reasoningCapabilitySource).toContain("from './markdown-rendering'"); + + expect(messageSource).toContain("from '@/capabilities/markdown-rendering'"); + expect(messageSource).not.toContain("from 'streamdown'"); + expect(messageSource).not.toContain("from '@streamdown/code'"); + + expect(reasoningSource).toContain("from '@/capabilities/reasoning-rendering'"); + expect(reasoningSource).not.toContain("from 'streamdown'"); + expect(reasoningSource).not.toContain("from '@streamdown/code'"); + }); + + it('routes code highlighting and the debug route through named capability boundaries', () => { + const codeBlockSource = readClientFile('components/ai-elements/code-block.tsx'); + const routerSource = readClientFile('router.tsx'); + const codeHighlightingSource = readClientFile('capabilities/code-highlighting.ts'); + const richCodeHighlightingSource = readClientFile('capabilities/rich-code-highlighting.ts'); + const debugSurfaceSource = readClientFile('routes/debug-surface.tsx'); + + expect(codeHighlightingSource).toContain("import('./rich-code-highlighting.js')"); + expect(codeHighlightingSource).toContain('export const preloadRichCodeHighlighter'); + expect(codeHighlightingSource).not.toContain("import { createHighlighter } from 'shiki'"); + expect(richCodeHighlightingSource).toContain("from 'shiki'"); + expect(codeBlockSource).toContain("from '@/capabilities/code-highlighting'"); + expect(codeBlockSource).toContain('preloadRichCodeHighlighter'); + expect(codeBlockSource).not.toContain("from 'shiki'"); + + expect(debugSurfaceSource).toContain("import('./ComponentDebug.js')"); + expect(routerSource).toContain("from './routes/debug-surface.js'"); + expect(routerSource).not.toContain("from './routes/ComponentDebug.js'"); + }); +}); diff --git a/src/client/components/EntitySidebar.tsx b/src/client/components/EntitySidebar.tsx index 8f542587..bdc69e92 100644 --- a/src/client/components/EntitySidebar.tsx +++ b/src/client/components/EntitySidebar.tsx @@ -1,37 +1,15 @@ -import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; - -type Decision = { id: number; project_id: number; content: string; rationale: string | null }; -type Assumption = { id: number; project_id: number; content: string }; - -type EntitiesData = { - decisions: Decision[]; - assumptions: Assumption[]; -}; +import type { WorkspaceDurableEntityState } from '@/workspace/workspace-controller-core'; const tabs = ['Decisions', 'Assumptions'] as const; type Tab = (typeof tabs)[number]; -export function useEntities(projectId: number) { - return useQuery({ - queryKey: ['entities', projectId], - queryFn: async () => { - const res = await fetch(`/api/projects/${projectId}/entities`); - if (!res.ok) throw new Error('Failed to fetch entities'); - return res.json(); - }, - }); -} - -export function EntitySidebar({ projectId }: { projectId: number }) { +export function EntitySidebar({ entityState }: { entityState: WorkspaceDurableEntityState }) { const [activeTab, setActiveTab] = useState('Decisions'); - const { data, isLoading } = useEntities(projectId); - - const decisions = data?.decisions ?? []; - const assumptions = data?.assumptions ?? []; + const { decisions, assumptions, isLoading } = entityState; return (
diff --git a/src/client/components/ai-elements/code-block.test.tsx b/src/client/components/ai-elements/code-block.test.tsx new file mode 100644 index 00000000..49bf5d58 --- /dev/null +++ b/src/client/components/ai-elements/code-block.test.tsx @@ -0,0 +1,183 @@ +// @vitest-environment happy-dom + +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const createPlainCodeTokensMock = vi.fn((code: string) => ({ + bg: 'transparent', + fg: 'inherit', + tokens: [[{ color: 'inherit', content: code }]], +})); +const getCachedHighlightedCodeMock = vi.fn(() => null); +const highlightCodeMock = vi.fn(); +const preloadRichCodeHighlighterMock = vi.fn(); + +vi.mock('@/capabilities/code-highlighting', () => ({ + createPlainCodeTokens: createPlainCodeTokensMock, + getCachedHighlightedCode: getCachedHighlightedCodeMock, + highlightCode: highlightCodeMock, + preloadRichCodeHighlighter: preloadRichCodeHighlighterMock, +})); + +describe('CodeBlockContent', () => { + beforeEach(() => { + createPlainCodeTokensMock.mockClear(); + getCachedHighlightedCodeMock.mockReset(); + getCachedHighlightedCodeMock.mockReturnValue(null); + highlightCodeMock.mockReset(); + preloadRichCodeHighlighterMock.mockReset(); + highlightCodeMock.mockResolvedValue({ + bg: '#111111', + fg: '#eeeeee', + tokens: [[{ color: '#ff0000', content: 'const answer = 42' }]], + }); + }); + + afterEach(() => { + cleanup(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it('renders plain code immediately before async highlighting completes', async () => { + let resolveHighlight: + | ((value: { + bg: string; + fg: string; + tokens: Array>; + }) => void) + | null = null; + + highlightCodeMock.mockImplementation( + () => + new Promise((resolve) => { + resolveHighlight = resolve as typeof resolveHighlight; + }), + ); + + const { CodeBlockContent } = await import('./code-block.js'); + + const { container } = render(); + + const pre = container.querySelector('pre'); + expect(pre).toBeTruthy(); + expect(screen.getByText('const answer = 42')).toBeTruthy(); + expect(pre?.style.backgroundColor).toBe('transparent'); + + await act(async () => { + resolveHighlight?.({ + bg: '#111111', + fg: '#eeeeee', + tokens: [ + [ + { color: '#ff0000', content: 'const' }, + { color: '#00ff00', content: ' answer = 42' }, + ], + ], + }); + }); + + await waitFor(() => { + expect(pre?.style.backgroundColor).toBe('#111111'); + expect(pre?.style.color).toBe('#eeeeee'); + }); + }); + + it('ignores stale async highlighting results after the code changes', async () => { + const pendingHighlights = new Map< + string, + (value: { bg: string; fg: string; tokens: Array> }) => void + >(); + + highlightCodeMock.mockImplementation( + (code: string) => + new Promise((resolve) => { + pendingHighlights.set( + code, + resolve as (value: { + bg: string; + fg: string; + tokens: Array>; + }) => void, + ); + }), + ); + + const { CodeBlockContent } = await import('./code-block.js'); + + const { rerender } = render(); + expect(screen.getByText('const first = 1')).toBeTruthy(); + + rerender(); + expect(screen.getByText('const second = 2')).toBeTruthy(); + + await act(async () => { + pendingHighlights.get('const first = 1')?.({ + bg: '#111111', + fg: '#eeeeee', + tokens: [[{ color: '#ff0000', content: 'STALE RESULT' }]], + }); + }); + + expect(screen.queryByText('STALE RESULT')).toBeNull(); + expect(screen.getByText('const second = 2')).toBeTruthy(); + + await act(async () => { + pendingHighlights.get('const second = 2')?.({ + bg: '#222222', + fg: '#ffffff', + tokens: [[{ color: '#00ff00', content: 'const second = 2' }]], + }); + }); + + await waitFor(() => { + expect(screen.getByText('const second = 2')).toBeTruthy(); + }); + }); + + it('preloads the rich highlighter when the user signals intent on the container', async () => { + const { CodeBlockContainer } = await import('./code-block.js'); + + const { container } = render( + + + , + ); + + const codeBlock = container.querySelector('[data-language="typescript"]'); + expect(codeBlock).toBeTruthy(); + + fireEvent.pointerEnter(codeBlock!); + fireEvent.focus(screen.getByRole('button', { name: 'Focus me' })); + + expect(preloadRichCodeHighlighterMock).toHaveBeenCalledTimes(2); + }); + + it('clears the copy-reset timer on unmount', async () => { + vi.useFakeTimers(); + const writeTextMock = vi.fn(async () => {}); + vi.stubGlobal('navigator', { clipboard: { writeText: writeTextMock } }); + + const { CodeBlock, CodeBlockCopyButton } = await import('./code-block.js'); + + const { container, unmount } = render( + + + , + ); + + await act(async () => { + fireEvent.click(container.querySelector('[data-slot="button"]')!); + await Promise.resolve(); + }); + + expect(writeTextMock).toHaveBeenCalledWith('const answer = 42'); + + unmount(); + + expect(() => { + vi.runAllTimers(); + }).not.toThrow(); + }); +}); diff --git a/src/client/components/ai-elements/code-block.tsx b/src/client/components/ai-elements/code-block.tsx index 72afd7ce..7f24cb18 100644 --- a/src/client/components/ai-elements/code-block.tsx +++ b/src/client/components/ai-elements/code-block.tsx @@ -3,9 +3,16 @@ import { CheckIcon, CopyIcon } from 'lucide-react'; import type { ComponentProps, CSSProperties, HTMLAttributes } from 'react'; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import type { BundledLanguage, BundledTheme, HighlighterGeneric, ThemedToken } from 'shiki'; -import { createHighlighter } from 'shiki'; +import { + createPlainCodeTokens, + getCachedHighlightedCode, + highlightCode, + preloadRichCodeHighlighter, + type CodeLanguage, + type CodeToken, + type TokenizedCode, +} from '@/capabilities/code-highlighting'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn } from '@/lib/utils'; @@ -19,7 +26,7 @@ const isUnderline = (fontStyle: number | undefined) => fontStyle && fontStyle & // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint interface KeyedToken { - token: ThemedToken; + token: CodeToken; key: string; } interface KeyedLine { @@ -27,7 +34,7 @@ interface KeyedLine { key: string; } -const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] => +const addKeysToTokens = (lines: CodeToken[][]): KeyedLine[] => lines.map((line, lineIdx) => ({ key: `line-${lineIdx}`, tokens: line.map((token, tokenIdx) => ({ @@ -37,7 +44,7 @@ const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] => })); // Token rendering component -const TokenSpan = ({ token }: { token: ThemedToken }) => ( +const TokenSpan = ({ token }: { token: CodeToken }) => ( & { code: string; - language: BundledLanguage; + language: CodeLanguage; showLineNumbers?: boolean; }; -interface TokenizedCode { - tokens: ThemedToken[][]; - fg: string; - bg: string; -} - interface CodeBlockContextType { code: string; } @@ -100,119 +101,6 @@ const CodeBlockContext = createContext({ code: '', }); -// Highlighter cache (singleton per language) -const highlighterCache = new Map>>(); - -// Token cache -const tokensCache = new Map(); - -// Subscribers for async token updates -const subscribers = new Map void>>(); - -const getTokensCacheKey = (code: string, language: BundledLanguage) => { - const start = code.slice(0, 100); - const end = code.length > 100 ? code.slice(-100) : ''; - return `${language}:${code.length}:${start}:${end}`; -}; - -const getHighlighter = ( - language: BundledLanguage, -): Promise> => { - const cached = highlighterCache.get(language); - if (cached) { - return cached; - } - - const highlighterPromise = createHighlighter({ - langs: [language], - themes: ['github-light', 'github-dark'], - }); - - highlighterCache.set(language, highlighterPromise); - return highlighterPromise; -}; - -// Create raw tokens for immediate display while highlighting loads -const createRawTokens = (code: string): TokenizedCode => ({ - bg: 'transparent', - fg: 'inherit', - tokens: code.split('\n').map((line) => - line === '' - ? [] - : [ - { - color: 'inherit', - content: line, - } as ThemedToken, - ], - ), -}); - -// Synchronous highlight with callback for async results -export const highlightCode = ( - code: string, - language: BundledLanguage, - // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks) - callback?: (result: TokenizedCode) => void, -): TokenizedCode | null => { - const tokensCacheKey = getTokensCacheKey(code, language); - - // Return cached result if available - const cached = tokensCache.get(tokensCacheKey); - if (cached) { - return cached; - } - - // Subscribe callback if provided - if (callback) { - if (!subscribers.has(tokensCacheKey)) { - subscribers.set(tokensCacheKey, new Set()); - } - subscribers.get(tokensCacheKey)?.add(callback); - } - - // Start highlighting in background - fire-and-forget async pattern - getHighlighter(language) - // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then) - .then((highlighter) => { - const availableLangs = highlighter.getLoadedLanguages(); - const langToUse = availableLangs.includes(language) ? language : 'text'; - - const result = highlighter.codeToTokens(code, { - lang: langToUse, - themes: { - dark: 'github-dark', - light: 'github-light', - }, - }); - - const tokenized: TokenizedCode = { - bg: result.bg ?? 'transparent', - fg: result.fg ?? 'inherit', - tokens: result.tokens, - }; - - // Cache the result - tokensCache.set(tokensCacheKey, tokenized); - - // Notify all subscribers - const subs = subscribers.get(tokensCacheKey); - if (subs) { - for (const sub of subs) { - sub(tokenized); - } - subscribers.delete(tokensCacheKey); - } - }) - // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks) - .catch((error) => { - console.error('Failed to highlight code:', error); - subscribers.delete(tokensCacheKey); - }); - - return null; -}; - const CodeBlockBody = memo( ({ tokenized, @@ -265,23 +153,44 @@ CodeBlockBody.displayName = 'CodeBlockBody'; export const CodeBlockContainer = ({ className, language, + onFocusCapture, + onPointerEnter, + onTouchStart, style, ...props -}: HTMLAttributes & { language: string }) => ( -
-); +}: HTMLAttributes & { language: string }) => { + const warmHighlighter = useCallback(() => { + void preloadRichCodeHighlighter(); + }, []); + + return ( +
{ + warmHighlighter(); + onFocusCapture?.(event); + }} + onPointerEnter={(event) => { + warmHighlighter(); + onPointerEnter?.(event); + }} + onTouchStart={(event) => { + warmHighlighter(); + onTouchStart?.(event); + }} + style={{ + containIntrinsicSize: 'auto 200px', + contentVisibility: 'auto', + ...style, + }} + {...props} + /> + ); +}; export const CodeBlockHeader = ({ children, className, ...props }: HTMLAttributes) => (
{ - // Memoized raw tokens for immediate display - const rawTokens = useMemo(() => createRawTokens(code), [code]); - - // Synchronous cache lookup — avoids setState in effect for cached results - const syncTokens = useMemo(() => highlightCode(code, language) ?? rawTokens, [code, language, rawTokens]); - - // Async highlighting result (populated after shiki loads) - const [asyncTokens, setAsyncTokens] = useState(null); - const asyncKeyRef = useRef({ code, language }); - - // Invalidate stale async tokens synchronously during render - if (asyncKeyRef.current.code !== code || asyncKeyRef.current.language !== language) { - asyncKeyRef.current = { code, language }; - setAsyncTokens(null); - } + const rawTokens = useMemo(() => createPlainCodeTokens(code), [code]); + const cachedTokens = useMemo(() => getCachedHighlightedCode(code, language), [code, language]); + const [asyncTokens, setAsyncTokens] = useState(cachedTokens); useEffect(() => { let cancelled = false; - highlightCode(code, language, (result) => { - if (!cancelled) { - setAsyncTokens(result); - } - }); + setAsyncTokens(cachedTokens); + + if (cachedTokens) { + return; + } + + void highlightCode(code, language) + .then((result) => { + if (!cancelled) { + setAsyncTokens(result); + } + }) + .catch((error) => { + console.error('Failed to highlight code:', error); + }); return () => { cancelled = true; }; - }, [code, language]); + }, [cachedTokens, code, language]); - const tokenized = asyncTokens ?? syncTokens; + const tokenized = asyncTokens ?? rawTokens; return (
@@ -396,7 +303,7 @@ export const CodeBlockCopyButton = ({ ...props }: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); - const timeoutRef = useRef(0); + const timeoutRef = useRef(null); const { code } = useContext(CodeBlockContext); const copyToClipboard = useCallback(async () => { @@ -410,7 +317,13 @@ export const CodeBlockCopyButton = ({ await navigator.clipboard.writeText(code); setIsCopied(true); onCopy?.(); - timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout); + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + timeoutRef.current = window.setTimeout(() => { + setIsCopied(false); + timeoutRef.current = null; + }, timeout); } } catch (error) { onError?.(error as Error); @@ -419,7 +332,10 @@ export const CodeBlockCopyButton = ({ useEffect( () => () => { - window.clearTimeout(timeoutRef.current); + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } }, [], ); diff --git a/src/client/components/ai-elements/message.test.tsx b/src/client/components/ai-elements/message.test.tsx new file mode 100644 index 00000000..bfed8e12 --- /dev/null +++ b/src/client/components/ai-elements/message.test.tsx @@ -0,0 +1,105 @@ +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@streamdown/cjk', () => ({ cjk: {} })); +vi.mock('@streamdown/code', () => ({ code: {} })); +vi.mock('@streamdown/math', () => ({ math: {} })); +vi.mock('@streamdown/mermaid', () => ({ mermaid: {} })); +vi.mock('streamdown', () => ({ + Streamdown: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +import { + MessageBranch, + MessageBranchContent, + MessageBranchNext, + MessageBranchPage, + MessageBranchSelector, + MessageBranchPrevious, + MessageToolbar, +} from './message.js'; + +function renderBranchFixture(children: React.ReactNode) { + return render( + + {children} + + + + + + + + , + ); +} + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe('MessageBranch', () => { + it('keeps the active branch index when equal-length branch content is replaced', () => { + const rendered = renderBranchFixture([ +
Alpha branch
, +
Beta branch
, + ]); + + fireEvent.click(screen.getByRole('button', { name: /next branch/i })); + expect(screen.getByText('Beta branch')).toBeTruthy(); + expect(screen.getByText('2 of 2')).toBeTruthy(); + + rendered.rerender( + + +
Gamma branch
+
Delta branch
+
+ + + + + + + +
, + ); + + expect(screen.queryByText('Beta branch')).toBeNull(); + expect(screen.getByText('Gamma branch').parentElement?.className).toContain('hidden'); + expect(screen.getByText('Delta branch').parentElement?.className).toContain('block'); + expect(screen.getByText('2 of 2')).toBeTruthy(); + }); + + it('clamps the active branch when the branch set shrinks', () => { + const rendered = renderBranchFixture([ +
Alpha branch
, +
Beta branch
, + ]); + + fireEvent.click(screen.getByRole('button', { name: /next branch/i })); + expect(screen.getByText('Beta branch')).toBeTruthy(); + + rendered.rerender( + + +
Only remaining branch
+
+ + + + + + + +
, + ); + + expect(screen.getByText('Only remaining branch').parentElement?.className).toContain('block'); + expect(screen.queryByText('2 of 2')).toBeNull(); + expect(screen.queryByRole('button', { name: /next branch/i })).toBeNull(); + }); +}); diff --git a/src/client/components/ai-elements/message.tsx b/src/client/components/ai-elements/message.tsx index 1f2c3755..9d8ae114 100644 --- a/src/client/components/ai-elements/message.tsx +++ b/src/client/components/ai-elements/message.tsx @@ -1,15 +1,11 @@ 'use client'; -import { cjk } from '@streamdown/cjk'; -import { code } from '@streamdown/code'; -import { math } from '@streamdown/math'; -import { mermaid } from '@streamdown/mermaid'; import type { UIMessage } from 'ai'; import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; import type { ComponentProps, HTMLAttributes, ReactElement } from 'react'; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { Streamdown } from 'streamdown'; +import { MarkdownRenderer } from '@/capabilities/markdown-rendering'; import { Button } from '@/components/ui/button'; import { ButtonGroup, ButtonGroupText } from '@/components/ui/button-group'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; @@ -95,12 +91,28 @@ interface MessageBranchContextType { totalBranches: number; goToPrevious: () => void; goToNext: () => void; - branches: ReactElement[]; + branchSignature: string; setBranches: (branches: ReactElement[]) => void; } const MessageBranchContext = createContext(null); +const getBranchSignature = (branches: ReactElement[]) => + branches + .map((branch, index) => { + const key = branch.key === null ? `index-${index}` : String(branch.key); + const type = (() => { + if (typeof branch.type === 'string') { + return branch.type; + } + + const componentType = branch.type as { displayName?: string; name?: string }; + return componentType.displayName ?? componentType.name ?? 'component'; + })(); + return `${key}:${type}`; + }) + .join('|'); + const useMessageBranch = () => { const context = useContext(MessageBranchContext); @@ -124,6 +136,8 @@ export const MessageBranch = ({ }: MessageBranchProps) => { const [currentBranch, setCurrentBranch] = useState(defaultBranch); const [branches, setBranches] = useState([]); + const totalBranches = branches.length; + const branchSignature = useMemo(() => getBranchSignature(branches), [branches]); const handleBranchChange = useCallback( (newBranch: number) => { @@ -134,25 +148,35 @@ export const MessageBranch = ({ ); const goToPrevious = useCallback(() => { - const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1; + const newBranch = currentBranch > 0 ? currentBranch - 1 : totalBranches - 1; handleBranchChange(newBranch); - }, [currentBranch, branches.length, handleBranchChange]); + }, [currentBranch, handleBranchChange, totalBranches]); const goToNext = useCallback(() => { - const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0; + const newBranch = currentBranch < totalBranches - 1 ? currentBranch + 1 : 0; handleBranchChange(newBranch); - }, [currentBranch, branches.length, handleBranchChange]); + }, [currentBranch, handleBranchChange, totalBranches]); + + useEffect(() => { + if (totalBranches === 0) { + return; + } + + if (currentBranch >= totalBranches) { + handleBranchChange(totalBranches - 1); + } + }, [currentBranch, handleBranchChange, totalBranches]); const contextValue = useMemo( () => ({ - branches, + branchSignature, currentBranch, goToNext, goToPrevious, setBranches, - totalBranches: branches.length, + totalBranches, }), - [branches, currentBranch, goToNext, goToPrevious], + [branchSignature, currentBranch, goToNext, goToPrevious, totalBranches], ); return ( @@ -165,15 +189,15 @@ export const MessageBranch = ({ export type MessageBranchContentProps = HTMLAttributes; export const MessageBranchContent = ({ children, ...props }: MessageBranchContentProps) => { - const { currentBranch, setBranches, branches } = useMessageBranch(); + const { currentBranch, setBranches, branchSignature } = useMessageBranch(); const childrenArray = useMemo(() => (Array.isArray(children) ? children : [children]), [children]); + const nextBranchSignature = useMemo(() => getBranchSignature(childrenArray), [childrenArray]); - // Use useEffect to update branches when they change useEffect(() => { - if (branches.length !== childrenArray.length) { + if (branchSignature !== nextBranchSignature) { setBranches(childrenArray); } - }, [childrenArray, branches, setBranches]); + }, [branchSignature, childrenArray, nextBranchSignature, setBranches]); return childrenArray.map((branch, index) => (
; - -const streamdownPlugins = { cjk, code, math, mermaid }; +export type MessageResponseProps = ComponentProps; export const MessageResponse = memo( ({ className, ...props }: MessageResponseProps) => ( - *:first-child]:mt-0 [&>*:last-child]:mb-0', className)} - plugins={streamdownPlugins} {...props} /> ), diff --git a/src/client/components/ai-elements/reasoning.tsx b/src/client/components/ai-elements/reasoning.tsx index 0701e8ce..cf4bba59 100644 --- a/src/client/components/ai-elements/reasoning.tsx +++ b/src/client/components/ai-elements/reasoning.tsx @@ -1,15 +1,11 @@ 'use client'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; -import { cjk } from '@streamdown/cjk'; -import { code } from '@streamdown/code'; -import { math } from '@streamdown/math'; -import { mermaid } from '@streamdown/mermaid'; import { BrainIcon, ChevronDownIcon } from 'lucide-react'; import type { ComponentProps, ReactNode } from 'react'; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Streamdown } from 'streamdown'; +import { ReasoningRenderer } from '@/capabilities/reasoning-rendering'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { cn } from '@/lib/utils'; @@ -182,8 +178,6 @@ export type ReasoningContentProps = ComponentProps & children: string; }; -const streamdownPlugins = { cjk, code, math, mermaid }; - export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => ( - {children} + {children} )); diff --git a/src/client/mutations/client-mutation.test.ts b/src/client/mutations/client-mutation.test.ts new file mode 100644 index 00000000..79cef28d --- /dev/null +++ b/src/client/mutations/client-mutation.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { ClientMutationError, postJsonMutation } from './client-mutation.js'; + +const fetchMock = vi.fn(); + +describe('client mutation', () => { + beforeEach(() => { + fetchMock.mockReset(); + vi.stubGlobal('fetch', fetchMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('surfaces network failures with the caller fallback message', async () => { + fetchMock.mockRejectedValueOnce(new TypeError('network down')); + + await expect( + postJsonMutation('/api/projects', { name: 'New project' }, 'Failed to create project'), + ).rejects.toEqual(new ClientMutationError('Failed to create project')); + }); + + it('falls back when an error response body is not json', async () => { + fetchMock.mockResolvedValueOnce( + new Response('upstream exploded', { + status: 502, + headers: { 'Content-Type': 'text/plain' }, + }), + ); + + await expect( + postJsonMutation('/api/projects', { name: 'New project' }, 'Failed to create project'), + ).rejects.toMatchObject({ + name: 'ClientMutationError', + message: 'Failed to create project', + status: 502, + }); + }); + + it('surfaces malformed success payloads as mutation errors', async () => { + fetchMock.mockResolvedValueOnce( + new Response('{', { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + await expect( + postJsonMutation('/api/projects', { name: 'New project' }, 'Failed to create project'), + ).rejects.toMatchObject({ + name: 'ClientMutationError', + message: 'Failed to create project', + status: 200, + }); + }); +}); diff --git a/src/client/mutations/client-mutation.ts b/src/client/mutations/client-mutation.ts new file mode 100644 index 00000000..5b71d225 --- /dev/null +++ b/src/client/mutations/client-mutation.ts @@ -0,0 +1,72 @@ +import { useMutation } from '@tanstack/react-query'; + +interface MutationErrorResponse { + error?: string; +} + +export class ClientMutationError extends Error { + readonly status: number | undefined; + + constructor(message: string, status?: number) { + super(message); + this.name = 'ClientMutationError'; + this.status = status; + } +} + +async function readMutationErrorMessage(response: Response, fallbackMessage: string): Promise { + try { + const payload = (await response.json()) as MutationErrorResponse; + if (typeof payload.error === 'string' && payload.error.trim().length > 0) { + return payload.error; + } + } catch { + // Fall back to the caller-provided message when the response is not JSON. + } + + return fallbackMessage; +} + +export async function postJsonMutation( + url: string, + body: TRequest, + fallbackMessage: string, +): Promise { + let response: Response; + + try { + response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } catch { + throw new ClientMutationError(fallbackMessage); + } + + if (!response.ok) { + throw new ClientMutationError(await readMutationErrorMessage(response, fallbackMessage), response.status); + } + + try { + return (await response.json()) as TResponse; + } catch { + throw new ClientMutationError(fallbackMessage, response.status); + } +} + +export function useClientMutation( + mutationFn: (variables: TVariables) => Promise, +) { + const mutation = useMutation({ mutationFn }); + + return { + run: async (variables: TVariables) => { + mutation.reset(); + return mutation.mutateAsync(variables); + }, + isPending: mutation.isPending, + errorMessage: mutation.error?.message ?? null, + clearError: mutation.reset, + }; +} diff --git a/src/client/mutations/project-mutations.ts b/src/client/mutations/project-mutations.ts new file mode 100644 index 00000000..d4076e38 --- /dev/null +++ b/src/client/mutations/project-mutations.ts @@ -0,0 +1,26 @@ +import { useNavigate } from '@tanstack/react-router'; + +import type { ProjectListItem } from '../../shared/api-types.js'; +import { postJsonMutation, useClientMutation } from './client-mutation.js'; + +export function useCreateProjectMutation() { + const navigate = useNavigate(); + const mutation = useClientMutation((variables: { name: string }) => + postJsonMutation( + '/api/projects', + variables, + 'Failed to create project', + ), + ); + + return { + createProject: async (name: string) => { + const project = await mutation.run({ name }); + void navigate({ to: '/project/$id', params: { id: String(project.id) } }); + return project; + }, + isPending: mutation.isPending, + errorMessage: mutation.errorMessage, + clearError: mutation.clearError, + }; +} diff --git a/src/client/mutations/workspace-mutations.ts b/src/client/mutations/workspace-mutations.ts new file mode 100644 index 00000000..6da716fd --- /dev/null +++ b/src/client/mutations/workspace-mutations.ts @@ -0,0 +1,44 @@ +import { useRouter } from '@tanstack/react-router'; + +import type { ProjectStateTurn } from '../../shared/api-types.js'; +import { findTurnOptionByPosition } from '../workspace/workspace-controller-core.js'; +import { postJsonMutation, useClientMutation } from './client-mutation.js'; + +export function useSelectTurnOptionMutation({ + projectId, + turn, + sendMessage, +}: { + projectId: number; + turn: ProjectStateTurn | undefined; + sendMessage: (message: { text: string }) => Promise | void; +}) { + const router = useRouter(); + const mutation = useClientMutation((variables: { turnId: number; position: number }) => + postJsonMutation<{ ok: boolean }, { position: number }>( + `/api/projects/${projectId}/turns/${variables.turnId}/select`, + { position: variables.position }, + 'Failed to save selection', + ), + ); + + return { + selectOption: async (position: number) => { + const selected = findTurnOptionByPosition(turn, position); + if (!selected || !turn) { + return; + } + + try { + await mutation.run({ turnId: turn.id, position }); + await router.invalidate(); + await sendMessage({ text: selected.content }); + } catch { + // The shared mutation hook surfaces the failure state in the UI. + } + }, + isPending: mutation.isPending, + errorMessage: mutation.errorMessage, + clearError: mutation.clearError, + }; +} diff --git a/src/client/router.tsx b/src/client/router.tsx index ebd2fa28..817fb779 100644 --- a/src/client/router.tsx +++ b/src/client/router.tsx @@ -1,9 +1,11 @@ import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router'; -import { ComponentDebug } from './routes/ComponentDebug.js'; +import type { ProjectListItem } from '../shared/api-types.js'; +import { DebugSurfaceRouteComponent } from './routes/debug-surface.js'; import { ExportPreview } from './routes/ExportPreview.js'; import { InterviewWorkspace } from './routes/InterviewWorkspace.js'; import { ProjectList } from './routes/ProjectList.js'; +import { fetchWorkspaceLoaderData } from './workspace/workspace-loader.js'; // Root layout const rootRoute = createRootRoute({ @@ -21,39 +23,16 @@ const indexRoute = createRoute({ loader: async () => { const res = await fetch('/api/projects'); if (!res.ok) throw new Error('Failed to load projects'); - return res.json() as Promise>; + return res.json() as Promise; }, component: ProjectList, }); -// GET /api/projects/:id → interview workspace +// GET /api/projects/:id + /entities → interview workspace const projectRoute = createRoute({ getParentRoute: () => rootRoute, path: '/project/$id', - loader: async ({ params }) => { - const res = await fetch(`/api/projects/${params.id}`); - if (!res.ok) throw new Error('Failed to load project'); - return res.json() as Promise<{ - project: { id: number; name: string; active_turn_id: number | null }; - turns: Array<{ - id: number; - answer: string | null; - question: string | null; - why: string | null; - impact: string | null; - phase: string; - user_parts: string | null; - assistant_parts: string | null; - options: Array<{ - id: number; - position: number; - content: string; - is_recommended: boolean; - is_selected: boolean; - }>; - }>; - }>; - }, + loader: async ({ params }) => fetchWorkspaceLoaderData(params.id), component: InterviewWorkspace, }); @@ -67,7 +46,7 @@ const exportRoute = createRoute({ const debugRoute = createRoute({ getParentRoute: () => rootRoute, path: '/debug', - component: ComponentDebug, + component: DebugSurfaceRouteComponent, }); const routeTree = rootRoute.addChildren([indexRoute, projectRoute, exportRoute, debugRoute]); diff --git a/src/client/routes/ComponentDebug.tsx b/src/client/routes/ComponentDebug.tsx index 2f6c413f..ad621c06 100644 --- a/src/client/routes/ComponentDebug.tsx +++ b/src/client/routes/ComponentDebug.tsx @@ -1,5 +1,4 @@ import { Link } from '@tanstack/react-router'; -import type { ToolUIPart, UIMessage } from 'ai'; import { useState, useCallback } from 'react'; import { @@ -34,7 +33,9 @@ import { Card, CardContent } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { cn } from '@/lib/utils'; -const FIXTURE_MESSAGES: UIMessage[] = [ +import type { BrunchUIMessage } from '../../shared/chat.js'; + +const FIXTURE_MESSAGES: BrunchUIMessage[] = [ { id: 'debug-1', role: 'user', @@ -64,51 +65,46 @@ const FIXTURE_MESSAGES: UIMessage[] = [ role: 'assistant', parts: [ { - type: 'tool-invocation', - toolInvocationId: 'debug-tool-1', - toolName: 'ask_question', + type: 'tool-ask_question', + toolCallId: 'debug-tool-1', state: 'output-available', - step: 0, - args: { + input: { question: 'What concurrency model should the event system use?', why: 'This determines how multiple observers can process events without blocking the main interview flow.', impact: 'high', options: [ { content: 'Single-threaded with async/await', is_recommended: true }, - { content: 'Worker threads for heavy extraction' }, - { content: 'Queue-based with retry semantics' }, + { content: 'Worker threads for heavy extraction', is_recommended: false }, + { content: 'Queue-based with retry semantics', is_recommended: false }, ], }, output: { - success: true, + ok: true, turnId: 42, + optionCount: 3, }, - } as unknown as ToolUIPart, + }, { type: 'text', - text: "Here's how the structured question would appear. The `ask_question` tool validates the output via Zod schema before persisting.", + text: "Here's how the structured question tool appears on the AI SDK-native stream before the workspace renders the matching turn card.", }, ], }, ]; -const FIXTURE_CODE = `export async function* conductTurn( - db: Database, - projectId: number, - userAnswer: string, -): AsyncIterable { - const turn = await db.createTurn(projectId, userAnswer); - - yield { type: 'turn-created', turnId: turn.id }; +const FIXTURE_CODE = `const stream = createUIMessageStream({ + async execute({ writer }) { + writer.merge( + interviewer.toUIMessageStream({ + sendReasoning: true, + sendFinish: false, + }), + ); - const response = await callAgent(turn); - - for (const entity of response.extractedEntities) { - await db.persistEntity(entity); - yield { type: 'entity-extracted', entity }; - } - - yield { type: 'turn-complete', turnId: turn.id }; + const entityIds = await runObserver(db, persistedTurn, projectId); + writer.write({ type: 'data-observer-result', data: { entityIds } }); + writer.write({ type: 'finish', finishReason: 'stop' }); + }, }`; const impactStyles: Record = { @@ -165,18 +161,13 @@ export function ComponentDebug() { ); } - if (part.type === 'tool-invocation') { - const toolPart = part as unknown as ToolUIPart & { toolName?: string }; + if (part.type === 'tool-ask_question') { return ( - + - - + + ); @@ -209,7 +200,7 @@ export function ComponentDebug() { ] as const ).map((state) => ( - + ))}
diff --git a/src/client/routes/InterviewWorkspace.test.tsx b/src/client/routes/InterviewWorkspace.test.tsx new file mode 100644 index 00000000..04d6565c --- /dev/null +++ b/src/client/routes/InterviewWorkspace.test.tsx @@ -0,0 +1,517 @@ +// @vitest-environment happy-dom + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { useCallback, useState } from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { EntitiesData, ProjectState } from '../../shared/api-types.js'; +import type { BrunchUIMessage } from '../../shared/chat.js'; +import type { WorkspaceLoaderData } from '../workspace/workspace-loader.js'; +import { InterviewWorkspace } from './InterviewWorkspace.js'; + +function createLiveQuestionMessage(): BrunchUIMessage { + return { + id: 'live-turn-assistant', + role: 'assistant', + parts: [ + { + type: 'tool-ask_question', + toolCallId: 'tool-1', + state: 'output-available', + input: { + question: 'Which platform should we target next?', + why: 'Platform shapes the first build.', + impact: 'high', + options: [ + { content: 'Web', is_recommended: true }, + { content: 'Desktop', is_recommended: false }, + ], + }, + output: { ok: true, turnId: 2, optionCount: 2 }, + }, + ], + }; +} + +type UseChatOptions = { + messages: BrunchUIMessage[]; + onData?: (dataPart: { type: string; data?: unknown }) => void; + onFinish?: () => void; +}; + +type UseChatHarness = { + sendMessage: ReturnType; + setMessages: ReturnType; + replaceMessages?: (messages: BrunchUIMessage[]) => void; + onData?: UseChatOptions['onData']; + onFinish?: UseChatOptions['onFinish']; +}; + +let currentLoaderData: WorkspaceLoaderData; +const routerInvalidate = vi.fn(async () => {}); +const fetchMock = vi.fn(); +let useChatImpl: (options: UseChatOptions) => { + messages: BrunchUIMessage[]; + sendMessage: (message: { text: string }) => Promise | void; + setMessages: (messages: BrunchUIMessage[]) => void; + status: 'ready' | 'submitted' | 'streaming'; +}; +let useChatHarness: UseChatHarness; + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children, ...props }: React.AnchorHTMLAttributes) => ( + {children} + ), + useLoaderData: () => currentLoaderData, + useParams: () => ({ id: String(currentLoaderData.projectState.project.id) }), + useRouter: () => ({ invalidate: routerInvalidate }), +})); + +vi.mock('@ai-sdk/react', () => ({ + useChat: (options: UseChatOptions) => useChatImpl(options), +})); + +vi.mock('ai', async () => { + const actual = await vi.importActual('ai'); + return { + ...actual, + DefaultChatTransport: class DefaultChatTransport { + constructor(_options: unknown) {} + }, + }; +}); + +vi.mock('@/components/ai-elements/conversation', () => ({ + Conversation: ({ children }: { children: React.ReactNode }) =>
{children}
, + ConversationContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + ConversationScrollButton: () => null, +})); + +vi.mock('@/components/ai-elements/message', () => ({ + Message: ({ children }: { children: React.ReactNode }) =>
{children}
, + MessageContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + MessageResponse: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock('@/components/ai-elements/prompt-input', () => ({ + PromptInput: ({ children }: { children: React.ReactNode }) =>
{children}
, + PromptInputBody: ({ children }: { children: React.ReactNode }) =>
{children}
, + PromptInputFooter: ({ children }: { children: React.ReactNode }) =>
{children}
, + PromptInputSubmit: () => , + PromptInputTextarea: () =>