diff --git a/.pi/POSTURE.md b/.pi/POSTURE.md new file mode 100644 index 000000000..d7ee9d81c --- /dev/null +++ b/.pi/POSTURE.md @@ -0,0 +1,6 @@ +certainty: proving +stakes: high +audience: internal +horizon: current-milestone +migration: free-rewrite +sourcing: strip-or-build diff --git a/archive/docs/archive/PLAN_HISTORY.md b/archive/docs/archive/PLAN_HISTORY.md deleted file mode 100644 index 4e829b36c..000000000 --- a/archive/docs/archive/PLAN_HISTORY.md +++ /dev/null @@ -1,204 +0,0 @@ -# Plan History - -Archived from the legacy phase-ledger form of `memory/PLAN.md` on 2026-04-14 during FE-584. - -## Completed Phases - -- 2026-04-14 — **Phase 1: Foundation** — walking skeleton proved SDK → SSE → React end to end, then SQLite persistence landed. -- 2026-04-14 — **Phase 2: Architecture** — turn-tree schema, Drizzle core extraction, and multi-project routing became the durable app spine. -- 2026-04-14 — **Phase 3: Interview Engine** — rich chat UI, structured scope interview, parts-based persistence, observer extraction, and the AI SDK pivot all shipped. -- 2026-04-14 — **Phase 4: Interaction + Knowledge Foundations** — streaming fixes, flexible turn responses, generic knowledge persistence, and phase-aware observer widening landed. -- 2026-04-14 — **Phase 5: Mode Closure + Full Interview** — explicit phase outcomes, canonical knowledge model, design mode, requirements review, and criteria review all closed the full interview loop. -- 2026-04-14 — **Phase 6: Readiness Surfaces + Export** — dashboard workflow state, knowledge workspace review surface, export, and richer fixture seeding shipped. -- 2026-04-14 — **Phase 7: Distribution + Brownfield + UI Alignment** — UI alignment, shiki/debug cleanup, local-first `npx` distribution, and brownfield kickoff all shipped. -- 2026-04-14 — **Phase 10: Route Ownership Refactor** — router seam characterization, route wrapper extraction, file-route infrastructure, and final route-directory consolidation all completed. -- 2026-04-14 — **Phase 11: Routing & Layout Refactor** — directory-based routing, three layout shells, per-phase views, entity sidebar relocation, and graph-view stub all completed. - -## Completed Hardening / Meta Work - -- 2026-04-14 — **Ad-hoc: Typing Hygiene** — Zod was removed from non-LLM boundaries while preserving LLM and HTTP validation seams. -- 2026-04-14 — **Phase 9 completed items** — launcher/runtime guard hardening (14b), trusted fixture hardening (16a), and capture-backed golden corpus work (16b) all shipped. - -- 2026-04-14 — **Phase terminal staging and auto-present current turn** — open phases auto-initiate the current turn, answered-turn replay filters control/closure artifacts, closed phases end with handoff/completion card. - -## Recent Frontier Archives - -- 2026-05-15 — **Side-chat persistence V4a retired as an independent frontier** — persistent side-chat history was absorbed into Conversational Workspace Runtime Track 2. After the chat/thread reconciliation, the near-term runtime uses durable secondary chats over the existing chat/turn substrate; schema-level `thread` is deferred until chat/turn proves insufficient. -- 2026-05-08 — **Side-chat V3.0 hard-impact cascade** — FE-674 / PR #115 + #116 + #117 shipped hard-impact cascade through `reconciliation_need`, Pending review listing, and idempotent resolve. Verified: `npm run verify` (1063 tests, 0 lint warnings). Watch moved forward to V3.1 / reconciliation-runtime walkthroughs. -- 2026-04-27 — **Runtime JSON payload hardening (FE-625)** — Express API parsing now accepts chat-sized request bodies above the default parser ceiling and returns a JSON 413 response instead of Express HTML when a payload exceeds the app limit. Verified: `npm run verify`. Watch: if real chat requests still exceed the 5 MB limit, investigate client history / tool-result pruning rather than only raising the ceiling. -- 2026-04-24 — **Distribution hardening release path (FE-531)** — `package.json` now declares the Node 22+ engine floor, explicit shipped files, and public scoped publish config; `npm run release` drives release-it at repo root, rebuilds and dry-runs the packaged artifact, and documents npm auth prerequisites. Verified: `npm run verify`. Watch: CI trusted publishing is still intentionally out of scope. -- 2026-04-23 — **Interviewer-autonomous question format with phase-aware gating** — revised D115: the interviewer now chooses whether to include options per-question based on conversational trajectory; observer interprets selections as resonance (grounding) or commitment (design); ActiveQuestionCard has phase-aware submit gating and "none of the above" copy. -- 2026-04-22 — **Review revision card contract consistency retired** — acceptance now carries predecessor metadata across sparse regenerated review sets, regeneration context/prompt sources preserve reference codes + rationale + grounding refs + explicit `Added in revision` / `Revised` semantics, and criteria-phase active/replayed/pending review-card routes plus source-owned examples prove the same contract as requirements. -- 2026-04-23 — **Brownfield workspace-analysis grounding brief parity / proving frontier retired** — retired `Active → Track A — Interaction model → Brownfield workspace-analysis grounding brief parity / proving` after a real brownfield start confirmed that the grounding brief, paired question, and live activity chrome read as one coherent turn lifecycle rather than three disconnected states. Follow-on interaction-model work now shifts to reusing that same context-gathering seam beyond the opening grounding turn instead of continuing to treat startup parity as an open proof question. -- 2026-04-22 — **Specification runtime state-machine proving frontier retired** — retired `Active → Track B — Runtime / workflow ownership → Specification runtime state-machine proving` after landing a lightweight specification-scoped lifecycle seam for deferred observer capture. Structured grounding/design responses now unlock their successor turn as soon as interviewer generation is durable, observer capture runs afterward through turn-owned `/api/specifications/:id/turns/:turnId/observer-capture`, unfinished capture reseeds from durable turns after reload, and late completion stays attached to the answered turn rather than the current frontier. Current limitations were kept explicit instead of papered over: server dedupe is process-local, and deferred capture is not yet generalized beyond grounding/design structured responses. -- 2026-04-22 — **Query ownership remediation frontier retired** — retired `Active → Track A — Query ownership → Query ownership remediation` after automated route/query ownership coverage plus manual walkthrough validation across `brownfield-grounding-replay`, `issue-tracker-requirements-ready`, `issue-tracker-criteria-ready`, and `issue-tracker-all-phases-closed`. The retired frontier established one authoritative specification bundle domain, one separately invalidable entities domain, and a transcript subtree no longer rendered by an entity-subscribing layout component. Follow-on observer-backlog / review-revision issues were recorded as a separate planning concern rather than reopening this retired frontier. -- 2026-04-22 — **Query ownership sub-slices archived out of live recently-completed state** — the fake `core` / `turns` split collapsed into one authoritative specification bundle domain, entities remained the separately invalidable domain, `/specification/$id/` redirect plus route priming moved onto the same bundle-owned helper, and route/query ownership integration oracles proved entities invalidation does not remount the interview route while bundle invalidation and direct navigation still use one authoritative fetch path. -- 2026-04-21 — **Review per-item commenting and regeneration** — per-item comment toggles on review set items, structured `itemComments` payloads, version badges, revision cards, and prior-revision collapsing all landed under the accepted-review frontier. -- 2026-04-21 — **Turn-internal grounding cards** — grounding-card plus question-card stacking landed within one turn lifecycle, with observer capture treating the validated turn as one unit. -- 2026-04-18 — **Accepted-set authority cleanup and legacy review semantics retirement** — replaced `reviewStatus`-driven downstream behavior with accepted-set projections, removed review badges from the UI, rewired requirements / criteria review prompts and submission semantics around explicit full-set review actions, and updated seeded walkthrough fixtures so confirmed review sets replay through the same accepted-set seam used at runtime. -- 2026-04-18 — **Explicit review-action payload seam for full-set review turns** — requirements and criteria review turns now carry explicit `reviewActions` metadata in the persisted tool payload, the submit path validates and persists the matching explicit `reviewAction`, targeted `requirementReview` / `criterionReview` turn metadata and per-item response writes are retired, and seeded review fixtures replay through the same contract. -- 2026-04-18 — **Phase header close action now follows the force-close policy seam** — the routed interview header now shows `Close Phase` only when `getForceClosePhaseAction()` says force-close is actually available, so review proposal states no longer expose a contradictory invalid close path while design keeps the existing typed force-close command. -- 2026-04-18 — **Review-phase proposal cards use review-specific acceptance copy** — proposal-pending requirements and criteria states in the routed interview view now render review-specific accept-the-reviewed-set copy instead of the generic closure-proposal presenter, while the confirmation flow still submits the same typed closure command payload. -- 2026-04-18 — **Review-phase kickoff and recovery cards use review-specific copy** — requirements and criteria kickoff / recovery states in the routed interview view now describe candidate-set review flow instead of generic interview-step copy, while the existing proceed / continue action wiring stays unchanged. -- 2026-04-18 — **Review-specific closed-phase completion cards** — closed requirements and criteria phases now render through `ReviewPhaseCompletionCard` in the routed interview view, so review closure states use review-specific handoff/export copy while non-review phases keep the generic closed-state shell. -- 2026-04-18 — **Dedicated active review-turn presenter split** — active requirements and criteria frontier turns now render through a dedicated `ActiveReviewSetCard` path instead of the generic `ActiveQuestionCard`, while persisted active turns and streamed pending turns still submit the same whole-set review action payload and preserve the shared `ReviewSetCard` boundary. -- 2026-04-18 — **Shared review-card route cutover with preserved card structure** — requirements and criteria review turns now render through the same `ReviewSetCard` path in both persisted and streamed pending states, while the richer shared card structure (stats and per-item comment affordances) remains intact even though review authority stays at the whole-set accept/request-changes seam. -- 2026-04-16 — **Specification-first creation and workspace-owned grounding kickoff** — new-spec creation now asks only for the specification name, the grounding strategy choice moved into the grounding kickoff inside the workspace, and touched entry/workspace copy now uses specification/workspace language while internal `project` identifiers remain unchanged. -- 2026-04-16 — **Frontier lifecycle skeleton across open phases** — the open-phase seam now bottoms out in fixed kickoff turns, visible generation states, same-turn review accept-to-close progression, and exceptional recovery turns, closing the no-dead-state frontier tracked under D94. -- 2026-04-16 — **Persist explicit full-set review actions through response + fixture seams** — requirements/criteria review submissions now carry explicit persisted `reviewAction` semantics, server acceptance no longer depends on option copy, client review submissions include the action in transport, and manifest/corpus/synthetic fixture seams round-trip the full-set review action without modeling per-item review turns as the user interaction. -- 2026-04-16 — **Criteria review accept-to-close wiring** — accepting the criteria full-set review now marks the presented criterion set approved, closes criteria on the same durable turn, makes the workflow output-ready, and suppresses the stale review text from being forwarded into chat after workflow completion. -- 2026-04-16 — **Lightweight review turn v1 across requirements + criteria** — both review phases now use full-set review turns with stable item reference codes, one review note, explicit `Accept review` / `Request changes` actions, and accept-to-close progression into the next kickoff/output frontier. -- 2026-04-16 — **Criteria full-set review turn parity** — criteria gained the same full-set review prompt/context/UI seam as requirements, including current criterion inventory, stable criterion reference codes, one review note, and explicit `Accept review` / `Request changes` actions. -- 2026-04-16 — **Requirements review accept-to-close wiring** — accepting the requirements full-set review now marks the presented requirement set approved, closes requirements on the same durable turn, creates the criteria kickoff frontier, and suppresses the stale review text from being forwarded into criteria chat. -- 2026-04-16 — **Transcript parity for existing turn families** — persisted assistant-side replay now stores concise activity summaries instead of raw reasoning / tool parts, hydrated answered / frontier cards reuse the same activity-placeholder family as live transcript updates, and route invalidation no longer needs generic placeholder fallbacks for existing turn families. Done: `npm run verify`. Watch: manual reload / invalidation walkthrough still outstanding. -- 2026-04-16 — **DrawerCard-based question card family and generating-turn placeholder** — ordinary interview turns now render through dedicated question-card components: compact answered cards, expanded active cards, inline activity placeholders, and a skeleton-backed generating-turn placeholder, replacing the older generic turn-card treatment for question-turn replay and in-flight generation. -- 2026-04-15 — **Center pane sticky header and ChatScroll integration** — `InterviewView` now renders phase metadata and state-gated actions in the sticky center header, with `ChatScroll` as the route-owned transcript container. -- 2026-04-15 — **Knowledge sidebar grouping registry** — `EntitySidebar` now groups visible knowledge kinds behind the hard-coded display registry with compact `DrawerCard` items and stable reference-code display. -- 2026-04-15 — **Phase stepper sidebar** — `PhaseNavigationSidebar` now renders the sticky specification header, sequential phase timeline, and conditional Output row. -- 2026-04-15 — **Top bar and phase label canonicalization** — RouteRoot gained the canonical top bar and shared phase-label registry across dashboard, sidebar, transcript copy, and fixtures. -- 2026-04-15 — **Story-first turn-card refinement** — DrawerCard, question/knowledge detail cards, chat transcript story, and token scale canon landed as the presentational base for route integration. -- 2026-04-14 — **Turn-owned captured-item projection and trailing observer attachment** — answered turns project captured knowledge with stable reference codes and keep late observer completion attached to the originating turn. -- 2026-04-14 — **Turn-owned submit/interviewer-processing choreography** — active turns stay mounted through submit, lock inline during processing, and collapse only when the next state is ready. -- 2026-04-14 — **Workspace shell first honesty pass** — dashboard links became real, root/dashboard scrolling was fixed, future phases became visible-but-disabled, review phases gained distinct shell framing, and transcript replay shifted from user bubbles toward compact answered-turn cards plus control markers. -- 2026-04-14 — **Fixture-backed walkthrough workspace** — walkthrough-ready seed scenarios now front-load the public seed catalog, prove resume after re-open, and cover export-ready/manual-inspection states. - -## 2026-04-18 Sync Archive - -Archived out of `memory/PLAN.md` during `ln-sync` once the live frontier narrowed to a few active cleanup tails plus the next major projector work. - -- 2026-04-18 — **Runtime review turns now persist an explicit review-set payload derived from the current review inventory** — `src/server/interview.ts` can now synthesize `data-review-set` payloads for requirements and criteria from the current review inventory, and `src/server/app.ts` now persists that synthesized review set onto runtime review turns even when the model only emits the structured question itself. -- 2026-04-18 — **Accepted review now materializes only the persisted review-set items instead of blanket-accepting all project-wide review entities** — `src/server/db.ts` can now materialize requirement / criterion items from a persisted `data-review-set`, reusing matching existing rows when present, and `src/server/app.ts` now prefers that accepted-review path over blanket-linking every project-wide requirement / criterion row. -- 2026-04-18 — **Requirement and criterion durability authority is now explicit in shared code** — `src/shared/knowledge.ts` codified the lifecycle boundary: exploration kinds stay observer-captured, while `requirement` and `criterion` are review-authoritative and tied to accepted review phases. -- 2026-04-18 — **Observer prompt and output schema now derive from shared ontology policy** — `src/server/observer.ts` now builds its output schema from the canonical knowledge registry and derives phase bias / kind-semantics prompt text from shared ontology policy. -- 2026-04-18 — **Shared observer ontology policy now declares phase-valid kinds and kind semantics** — `src/shared/knowledge.ts` now exports one phase-by-phase observer ontology policy plus per-kind semantic-role text. -- 2026-04-18 — **Story, fixture, and demo samples now teach the canonical reference-code contract** — seeded review-set fixture scenarios and chat/review/turn-lifecycle story fixtures now derive visible knowledge codes from `createKnowledgeReferenceCode()`. -- 2026-04-18 — **Canonical reference-code prefixes now emit the intended short-form contract** — the shared knowledge registry now emits `G` / `T` / `CTX` / `CON` / `R` / `AC` / `D` / `A`, and UI fallback badges consume the shared prefix map. -- 2026-04-18 — **Reference-code test fixtures now derive from the shared generator instead of freezing literals** — high-churn UI/server/shared tests now call `createKnowledgeReferenceCode()` for incidental codes. -- 2026-04-18 — **Registry-owned reference-code prefixes now drive runtime code generation** — `src/shared/knowledge.ts` now stores the current reference-code prefix on each `knowledgeKindRegistry` entry and derives `createKnowledgeReferenceCode()` from registry metadata. -- 2026-04-18 — **Captured-item replay now projects through one collection-driven entity path** — `src/server/db.ts` now builds `captured_items` for replay by iterating the canonical project-wide entity collections through `knowledgeKindRegistry`. -- 2026-04-18 — **Decision and assumption transport schemas now derive from the canonical knowledge-item contract** — `src/shared/api-types.ts` now defines decision and assumption entity schemas by projecting `knowledgeItemSchema`. -- 2026-04-18 — **Decision and assumption entity reads now share one knowledge-item projection helper** — `src/server/db.ts` no longer uses bespoke `toDecision` / `toAssumption` adapters for entity projection. -- 2026-04-18 — **Active-path generic entity filtering now runs through one collection-driven helper** — `src/server/db.ts` now filters generic knowledge collections for active-path projection through a shared registry-driven helper. -- 2026-04-18 — **Shared kind lookup maps now drive both sidebar and server relationship projection** — `src/shared/knowledge.ts` now exports canonical `kind -> collectionKey` and `kind -> entityCollection` maps consumed by both UI and server projection seams. -- 2026-04-18 — **Project-wide generic entity projection now uses one shared knowledge-item path** — `src/server/db.ts` now projects `requirement` and `criterion` through the same generic knowledge-item helper used for the other canonical `knowledge_item` kinds. -- 2026-04-18 — **Shared ontology tuples now drive API kind enums and manifest collection mapping** — `src/shared/knowledge.ts` now exports canonical knowledge-kind / collection tuples plus `knowledgeCollectionKeyByKind`, and manifest seeding now consumes the shared kind→collection mapping. -- 2026-04-18 — **Non-compatibility `framing` references retired from active fixtures and test naming** — route infrastructure tests now distinguish canonical grounding routes from the legacy `/framing` redirect seam, server observer/app tests use canonical `context` naming, and the active issue-tracker manifest no longer describes criteria review in framing-era language. -- 2026-04-18 — **Shared requirement/criterion entity contracts dropped legacy `reviewStatus`** — `src/shared/api-types.ts` now exposes canonical requirement and criterion entities without `reviewStatus`, and client/sidebar/graph/interview fixtures now use the canonical read-model shape. -- 2026-04-18 — **Distinct review-phase UI rebuilt on accepted-set authority** — requirements and criteria now render through review-specific entry, active, replayed, proposal, and closed-state presenters, while the routed header close action follows the same force-close policy seam as the transcript. -- 2026-04-18 — **Canonical grounding route cut over with legacy framing redirect** — the first phase entered through `/grounding`, index/export/in-workspace navigation targeted the canonical grounding URL, the file-routed interview surface gained a dedicated `grounding.tsx` entry, and the legacy `/framing` route was reduced to a temporary redirect seam until full retirement. -- 2026-04-19 — **Legacy knowledge facade cleanup retired** — decision/assumption entity references unified on `knowledge_item`, dead legacy per-type schema/relationship tables were removed, and migration `0010_retire_legacy_knowledge_tables` made the canonical storage seam authoritative for boot, seeding, and projection. - -## 2026-04-19 Frontier Retirement Archive - -Archived out of `memory/PLAN.md` when the phase-transition / handoff frontier retired and the live plan advanced to naming normalization. - -- 2026-04-19 — **Legacy fixture side path removed; one TS-native fixture model remains** — walkthroughs, app tests, and observer probes now seed through direct TypeScript builders/helpers only. -- 2026-04-19 — **Force-close and close-confirmation now read as explicit in-flight control actions** — typed proposal confirmations and design force-close requests now surface control markers in the workspace stream while stale proposal/frontier projection is suppressed during submit. -- 2026-04-19 — **Closed-phase stream artifacts now read as explicit completion / handoff states** — accepted closure replay now renders through dedicated completion chrome instead of the generic workspace-state shell, and non-review closed-phase handoffs carry explicit handoff framing in the workspace stream. -- 2026-04-19 — **Public walkthrough seeds now prefer canonical review-turn helpers over stale late-phase scenario slices** — the public `requirements-ready` / `criteria-ready` seeds now resolve through the helper-backed full-set review seams, and the `issue-tracker-*` kickoff-ready later-phase walkthrough fixtures no longer slice stale per-item requirement drafts into public truth. Walkthrough regression coverage now asserts persisted `data-review-set` metadata on the seeded requirements / criteria review turns. -- 2026-04-19 — **D113 rejected auto-submit hardening landed under the handoff frontier** — specification-scoped auto phase intents now treat rejected submit promises as failed submissions instead of leaving the reachable phase stuck in lifecycle-owned generating state. The helper marks the current auto intent failed, re-projects the kickoff/recovery control, and still suppresses duplicate retry across rerender/remount until durable landing changes. -- 2026-04-19 — **D113 recovery auto-continue proving slice landed under the handoff frontier** — current reachable recovery restoration now auto-submits typed `phase-continue` through the same specification-scoped lifecycle helper as kickoff auto-present, suppresses duplicate submit across rerender/remount, and falls back to the projected recovery card after failed auto-submit instead of retry-looping. Router ownership of navigation plus durable read-model rendering remains unchanged, and no second durable workflow model was introduced. -- 2026-04-19 — **Narrow D113 lifecycle proving slice landed under the handoff frontier** — current reachable kickoff auto-present now runs through a specification-scoped lifecycle helper rather than a route-local effect, uses typed phase-entry intent submission only, suppresses duplicate submit across rerender/remount, and preserves router ownership of navigation plus durable read-model rendering. Grounding strategy kickoff on `scope` remained explicit pending the follow-on lifecycle slices. -- 2026-04-19 — **Turn-artifact persistence and brownfield replay hardening retired from active execution** — interviewer-owned review, grounding, activity, and closure artifacts now materialize through one server-owned seam, active review buttons submit by semantic action metadata instead of assumed option order, and the walkthrough catalog now includes a named brownfield reusable-grounding replay scenario. -- 2026-04-19 — **Interaction-family canonicalization retired from the active frontier** — the workspace stream now carries projected kickoff/recovery/handoff controls plus durable grounding/question/review turns as one canonical interaction family, and brownfield grounding no longer depends on a one-shot repo-summary question ritual. -- 2026-04-19 — **Merged-stream projector cutover retired from the active frontier** — project creation, phase confirmation / force-close, and requirements acceptance no longer pre-seed next-phase kickoff rows; resumed state, phase-entry kickoff, and closed-phase advancement now rely on derived `landing` plus durable workflow outcomes instead of fabricated control rows. - -## 2026-04-19 Sync Archive - -Archived out of `memory/PLAN.md` during `ln-sync` once the live frontier narrowed to the handoff frontier plus only the most recent retirement markers. - -- 2026-04-19 — **Brownfield grounding-card opening sequence landed** — brownfield kickoff now begins with a provisional grounding card instead of a repo-summary question handoff, grounding cards replay as their own workspace-stream turn family, and observer capture skips answered grounding-card continues while still advancing to the next interviewer turn. -- 2026-04-19 — **Transitional control-row runtime plumbing was retired behind derived landing + typed phase intent** — legacy control-row fabrication left production helpers, phase-intent chat submits now prepare interviewer turns directly from derived landing, control markers depend only on typed `data-phase-intent`, and projected kickoff/recovery controls submit through the shared phase-intent seam rather than branching on persisted control rows. -- 2026-04-19 — **Workspace-stream projection consolidated around one bottom-artifact and ordered artifact seam** — the routed interview surface moved to one discriminated bottom-artifact contract, stream ordering now projects through one client seam, projected control artifacts use control/artifact terminology, and review/control/handoff markers render inline in the ordered workspace stream. -- 2026-04-19 — **Landing derivation and seed-first walkthrough authority replaced canonical control rows** — specification-state reads stay projection-only, open-phase landing now derives from workflow state plus active-path turns, fixture/corpus/manifest seams normalize to derived landing, canonical transition fixtures seed durable authority instead of authoritative control rows, and projected kickoff strategy selection no longer requires a seeded kickoff turn row. -- 2026-04-19 — **Legacy route and knowledge-facade cleanup retired** — the legacy `/framing` compatibility route was removed from the active app surface, and the last legacy knowledge-facade/schema cleanup landed so runtime boot, seeding, and projection now flow only through the canonical knowledge seams. -- 2026-04-18 — **Runtime-generated review turns now persist their own interviewer-owned review metadata** — review turns can carry explicit `reviewActions` plus a durable `reviewSet`, and the happy path now replays and accepts that authoritative runtime metadata instead of relying on synthesized fallback inventory. - -## 2026-04-21 Live-plan cleanup archive - -Archived out of `memory/PLAN.md` when the active frontier moved from the mostly-landed dramaturgical hardening bundle to the next grounding interaction-model slices, and older completion entries were trimmed back to the last three completed items. - -- 2026-04-20 — **Canonical `grounding` workflow key landed under the naming frontier** — the first phase now uses `grounding` across shared contracts, persistence/runtime logic, fixtures, tests, and export/read-model seams instead of preserving `scope` as the internal key. -- 2026-04-20 — **Canonical specification-named browser and HTTP path family landed under the naming frontier** — routed workspace/export entry moved through `/specification/...`, client fetch/mutation seams targeted `/api/specifications/...`, and legacy `/project/...` plus `/api/projects/...` paths survived only as explicit compatibility seams. -- 2026-04-20 — **Client-owned terminology cleanup slices landed under the naming frontier** — client-facing state seams defaulted to `Specification*` aliases, specification/workspace helper and module names replaced the remaining client runtime `project` wording, and exhausted execution-queue artifacts were retired without changing DB identifiers. -- 2026-04-19 — **Phase transition and handoff stabilization retired from the active frontier** — requirements acceptance now advances directly into criteria kickoff, criteria acceptance closes the workflow into export-ready state, and closed phases project explicit handoff/completion artifacts. - -## 2026-04-21 Sync archive - -- 2026-04-21 — **Grounding free-text question format with hint-guided prompts** — grounding questions use open free-text format; hint-guided priority-ordered topic list replaces unconstrained prompt; schema, prompt, response, and observer seams all aligned. Traceability: D115, D120; A59, A63; Requirements 4, 27. -- 2026-04-21 — **Homepage workspace binding** — root route surfaces workspace (CWD) identity with workspace name + path. Traceability: D122; Requirement 26. -- 2026-04-20 — **Alias deletion retired the naming frontier** — removed remaining `/api/projects/...` compatibility entry points and deleted shared/server `project` alias seams. -- 2026-04-20 — **Specification routes moved to canonical ownership** — routed workspace/export entry through `/specification/...` and client seams through `/api/specifications/...`. - -## 2026-04-23 Sync archive - -- 2026-04-22 — **Transcript/entity boundary repair** — moved the entities subscription out of `src/client/routes/specification/$id/_view/route.tsx`'s transcript-owning `ViewLayout` into entity-owned child surfaces only, and strengthened the mounted-route router oracle to prove entities invalidation refetches only `/entities` without remounting or rerendering the interview route. Verified: `npm run verify`. - -## 2026-04-23 Plan revision archive - -Archived when Track A (interaction model) completed and the frontier shifted to Track B (infrastructure). SPEC.md was pruned: 8 embedded assumptions and 33 embedded decisions retired from the live register. - -- 2026-04-23 — **Phase- and mode-agnostic context gathering** — `present_preface` + exploration tools available in all phases when `cwd` is present (not just brownfield grounding); lightweight context-gathering addendum appended to all phase prompts; "grounding card" terminology replaced with "preface card" in code, tests, and canonical docs. -- 2026-04-23 — **Interviewer-autonomous question format with phase-aware gating** — revised D115 so the interviewer chooses whether to include options per-question; observer interprets selections as resonance in grounding, commitment in design; ActiveQuestionCard has phase-aware submit gate and "none of the above" copy. -- 2026-04-23 — **Brownfield workspace-analysis grounding brief parity / proving retired** — real brownfield start confirmed the grounding brief, paired question, and live activity chrome read as one coherent turn lifecycle. -- 2026-04-23 — **Transcript activity chrome and workspace polish** — task activity mirrors reasoning's auto-open/auto-collapse behavior, live tool activity surfaces richer target details during streaming, duplicate `src/components/ai-elements` tree removed. - -## 2026-04-27 Sync archive - -- 2026-04-24 — **Compiled CLI runtime boundary for distribution hardening** — `npm run build` emits `dist/server/cli.js`, `bin/brunch.js` targets the compiled runtime, and build-backed package-bin smoke coverage proves help-path execution plus local-first launcher startup against the built client artifact. -- 2026-04-23 — **Phase- and mode-agnostic context gathering** — `present_preface` + exploration tools became available in all phases when `cwd` is present, and "grounding card" terminology was replaced with "preface card". -- 2026-04-23 — **Interviewer-autonomous question format with phase-aware gating** — the interviewer chooses whether to include options per question; observer capture interprets selections phase-appropriately while the client keeps grounding free-text-required and design selection-gated. -- 2026-04-23 — **SPEC.md pruning** — retired embedded assumptions and decisions from the live register, leaving only active uncertainty, seam-defining decisions, and future-facing constraints. - -## 2026-04-30 Sync archive - -- 2026-04-24 — **Workflow ownership extraction** — workflow projector extraction, turn-response transition extraction, chat-route transition/application extraction, and phase-close / force-close write-path ownership now live behind runtime-owned seams. Verified: `npm run verify`. - -## 2026-05-05 Sync Archive - -Archived out of `memory/PLAN.md` during design-doc reconciliation once the live frontier narrowed to continuous workspace plus only the last three completed items. - -- 2026-04-24 — **Distribution hardening release path** — `package.json` declares the Node 22+ engine floor, explicit shipped files, and public scoped publish config; `npm run release` drives release-it at repo root, rebuilds and dry-runs the packaged artifact, and documents npm auth prerequisites. Verified: `npm run verify`. Watch: CI trusted publishing remains intentionally out of scope. - -## 2026-05-07 Sync Archive - -Archived out of `memory/PLAN.md` after FE-697 multi-chat substrate landed and FE-673 side-chat V2 plumbing reached branch-complete. Three older completed entries rotated out to keep the live ledger at the last three items. - -- 2026-04-30 — **FE-639 relation-first observer capture first cut** — eligible answered turns enter one background observer-capture backlog, observer prompts use compact existing-knowledge anchors, observer output persists validated graph-delta relationship candidates, and accepted review grounding refs reuse the same conservative relation policy. Verified: `npm run verify`. Watch: A66 remains open until corpus/manual graph-review proves edge precision and density are useful. -- 2026-04-29 — **Workflow ownership extraction (FE-616)** — workflow projector extraction, turn-response transition extraction, chat-route transition/application extraction, and phase-close / force-close write-path ownership now live behind runtime-owned seams. Verified: `npm run verify`. Unblocks continuous workspace. -- 2026-04-27 — **Runtime JSON payload hardening** — Express API parsing now accepts chat-sized request bodies above the default parser ceiling and returns a JSON 413 response instead of Express HTML when a payload exceeds the app limit. Verified: `npm run verify`. Watch: if real chat requests still exceed the 5 MB limit, investigate client history / tool-result pruning rather than only raising the ceiling. - -Use `memory/PLAN.md` for the live frontier only. - -## 2026-05-11 Sync Archive - -Archived out of `memory/PLAN.md` after side-chat V3.1 closed end-to-end (PR #124) and the live ledger narrowed to V3.0 + V3.1 + the multi-chat substrate. The **six** dated `-` bullets immediately below (2026-05-08 through 2026-05-01) rotate prior ledger rows out of the live plan; the first bullet is the slice-4 classifier ship note folded into the V3.1 closure. - -- 2026-05-08 — **Side-chat V3.1 slice 4 — reconciliation classifier (schema + run-agent route)** (FE-674) — added three nullable agent_* columns on `reconciliation_need` (migration 0019); pure `classifyNeed()` over a stubbed-or-live AI SDK adapter; `POST /api/specifications/:id/reconciliation-needs/run-agent` walks every awaiting open need through the lifecycle `null → queued → classifying → classified | failed`; new `reconciliation-classifier.md` prompt asset registered in the prompt-loader; listing endpoint now exposes the three classifier columns with null defaults. SPEC.md gains I114 and the seed corpus location at `src/server/__corpus__/reconciliation-classifier-seeds.json`. Verified: `npm run verify` (1126 passed, +17 net new tests). Folded into V3.1 closure on 2026-05-11. -- 2026-05-08 — **FE-674 planning sync** — reconciled `docs/design/SIDE_CHAT.md` §5.3 / §8 / §9 / §13 against the downstack FE-697 substrate; SPEC.md adds A88 (Path 1 sufficiency without agent), D146 (cascade routes through `reconciliation_need`, `deferred: true` apply contract removed at V3.0 ship), I113 (apply opens at least one need per typed dependency edge), and rewrites Acceptance Criterion 7. Doc-only, no `src/` touched. PR #110 stacked on FE-704. -- 2026-05-07 — **FE-698 prompt/context scenario substrate** — Packaged markdown prompt registry + observer context-pack foundation + scenario runner capture skeleton/composition + agent mutation-surface audit. Server interviewer, observer, and side-chat role prompts now load from markdown assets through a typed prompt registry, observer capture renders its existing prompt context through the first typed scenario-specific context pack, and seeded observer-capture prompt scenarios now compose the production observer prompt with typed context-pack output into deterministic no-provider probe artifacts. Verified: `npm run verify` for code slices; audit verified by code-search/document consistency. -- 2026-05-07 — **Side-chat V2 — Edit / Drill-down / Propose-edge plumbing** (FE-673, PR #97) — added `edit`, `edge`, and `drill-down` patch kinds. Server `classifyEditImpact` returns `none | soft | hard`; soft applies directly with undo, hard returns `deferred: true` placeholder (removed at V3.0 ship). Client: patch-list reducer + three applier factories with real undo handlers. Verified: `npm run verify` (935 tests, 19 new). -- 2026-05-04 — **Graph view structured-list peer route** — `/specification/$id/graph` now renders project-wide entities through the structured-list layout with relationship subsections, relation chips, empty state, row controls, and a back-to-chat affordance. Follow-up active-path filtering and spatial canvas remain horizon work. -- 2026-05-01 — **Side-chat V1.1 — Explore vertical slice** — end-to-end graph-launched chat interaction shipped: prompt builder, POST `/side-chat` SSE endpoint, popover host, graph-view wiring, SSE consumer, and active-button activation. Follow-up refactor collapsed pending assistant text into the message list and extracted `SideChatHost` so activation is a tree-mount fact. - -## 2026-05-13 Sync Archive - -Archived out of `memory/PLAN.md` during `ln-sync` so the live plan keeps only the rolling frontier plus the last three completed items. Entries already archived in the 2026-05-11 sync archive were not duplicated here. - -- [2026-05-08] FE-698 prompt/context follow-up hardening — Candidate-spec prompt scenarios no longer advertise durable changeset submission, prompt scenario artifacts report schema version 2 for the fingerprinted shape, scenario definitions require typed context data, empty prompt assets are cached correctly, context-pack anchors use intent vocabulary, and `context-pack.ts` now remains the public entry point over private scenario-specific context-pack modules. Verified: `npm run verify`. Watch: this is still FE-698 continuation hardening; broader generative quality review and additional scenario probes remain later slices. -- [2026-05-08] FE-698 prompt/context remediation + candidate scenario — Prompt scenario definitions are now discriminated by scenario kind, candidate-spec scenarios render deterministic no-provider proposal artifacts from typed context packs, scenario artifacts include prompt/context fingerprints, server prompt asset copying mirrors current source assets, prompt golden coverage protects production prompt text, and the build-boundary prompt test writes isolated output. Verified: `npm run verify`. Watch: full generative quality review for candidate-spec output remains a later execution/probe slice. -- [2026-05-08] FE-698 scenario execution error hardening — Scenario execution failures now serialize safe deterministic summaries: API-key-like provider errors are redacted, non-Error rejections avoid object dumps, and ordinary errors remain reviewable. Verified: `npm run verify`. -- [2026-05-08] FE-698 Anthropic scenario adapter — Added a probe-only Anthropic AI SDK adapter behind the existing `PromptScenarioModelAdapter` seam. Web-research prompt scenarios now map rendered prompts to AI SDK system content and rendered context packs to user prompt content under mocked tests, with unsupported providers rejected before model construction. Verified: `npm run verify`. Watch: this is not the shared AI runtime provider seam; OpenRouter/provider-neutral routing, credential UX, Pi, web tools, CLI/UI, persistence, and Brunch mutations remain out of scope. -- [2026-05-08] FE-698 prompt scenario execution probe — Web-research prompt scenarios can now execute through an injected fakeable model adapter and serialize `succeeded` / `failed` execution results with raw output or deterministic error text, while no-provider artifacts remain deterministic `not-run` snapshots. Structured parsing is explicitly `not-applicable` for this prose-only web-research path. Verified: `npm run verify`. Watch: real provider adapters, Pi, web tools, CLI/UI, persistence, and mutating Brunch handlers remain out of scope for this foundation slice. -- [2026-05-06] Multi-chat substrate + reconciliation needs (FE-697) — `chat` table with one interview chat per spec, nullable `turn.chat_id`, `specification.primary_chat_id`, mirrored `chat.active_turn_id`, plus the `reconciliation_need` queue with directed source/target items, narrow `kind`/`status`, partial unique index on open rows, cascade FK. Spec creation inserts spec + interview chat in one transaction; `advanceHead` is transactional. No user-visible change. Verified: `npm run verify` (673 tests) plus manual fixture playback (39 specs / 81 turns / dual-pointer equivalence). A82 / A83 validated for Phase 1. -- 2026-05-20 — **Pre-POC archive and reseed** — razed pre-POC implementation, archived legacy docs and planning memory under `archive/`, tagged `next-baseline`, and reseeded `memory/SPEC.md` and `memory/PLAN.md` from the three canonical POC architecture docs. Phase 3 infra bootstrap was folded into `walking-skeleton` rather than remaining an independent frontier. - -## 2026-05-22 Sync archive - -Archived out of `memory/PLAN.md` when `web-shell` closed and the live frontier advanced to `graph-data-plane`. - -- 2026-05-22 — **web-shell judo review fixes** — Session projection reads share a canonical Brunch session envelope, prompt-side custom-entry classification uses an explicit allowlist, and the React shell builds transcript query params from a typed session projection target without non-null assertions. Verified: `npm run verify` after each slice. -- 2026-05-22 — **web-shell tie-off queue** — Explicit session projection rejects ambiguous self-description (`brunch.session_binding` duplicates, missing/duplicate Pi headers, binding/header session-id mismatch); `session.transcriptDisplay` includes displayable transcript-native `brunch.elicitation_prompt` rows; M3 browser-open smoke debt was adjudicated as environment-blocked after direct HTTP/WebSocket postconditions passed. -- 2026-05-22 — **web-shell hardening slices** — Shared JSON-RPC protocol helpers, `ws`-backed `/rpc` transport, persistent browser RPC multiplexing, traversal-safe static asset serving, stable React runtime ownership, and explicit read-only session projection by durable session id landed without REST product reads or connection-as-session semantics. -- 2026-05-21 — **web-shell initial slices** — Linear transcript policy hardening landed before browser consumption, transcript readers fail fast on non-linear Pi JSONL, the minimal native web HTTP shell and WebSocket RPC bridge came online, and the React shell rendered `workspace.snapshot` chrome via one WebSocket RPC client. -- 2026-05-20 — **walking-skeleton** — Brunch launches through a pi-backed TUI boot path with coordinator-first spec gating, project-local `.brunch/` state, self-describing Pi JSONL sessions, same-spec `/new`, persistent chrome through pi's extension widget seam, a bin shim, and the store-only runbook checker. Verified: `npm run verify`, manual TUI smoke, automated TUI/coordinator tests, and runbook oracle. diff --git a/archive/docs/archive/design/INTENT_SPEC_EVOLUTION.md b/archive/docs/archive/design/INTENT_SPEC_EVOLUTION.md deleted file mode 100644 index 68795ec9d..000000000 --- a/archive/docs/archive/design/INTENT_SPEC_EVOLUTION.md +++ /dev/null @@ -1,821 +0,0 @@ -# Brunch Evolution Notes | @Yesterday - -> Status: **source archive / raw synthesis**. -> Archived from `docs/design/` during FE-705 reconciliation cleanup on 2026-05-13. Active conclusions now live in `memory/SPEC.md`, `memory/PLAN.md`, and focused design docs under `docs/design/`. -> -> Canonical conclusions must be promoted into `memory/SPEC.md` through `ln-spec` and into `memory/PLAN.md` through `ln-plan` before they are treated as accepted product direction or roadmap work. -> -> Synthesis started 2026-05-04 from external agent conversations about intent formalization, formal verification, and Brunch's elicitation methodology. -> - - - - - -## Why this note exists - -Brunch currently elicits and maintains software specifications through a structured but mostly prose-centered workflow. The conversations captured so far suggest a stronger frame: - -> Brunch should move from eliciting planning specs toward eliciting intent specs: structured, progressively checkable claims about what correctness would mean, what remains uncertain, and how the user validated or rejected competing interpretations. -> - -This note separates the broad areas that are emerging so subsequent planning can be collated against them. - -# Themes and concepts - -## 1. From Planning Specs to Intent Specs - -A planning spec is optimized for downstream work sequencing: - -- what to build -- what matters next -- what scope is in or out -- which implementation slices follow - -An intent spec is optimized for preserving and validating meaning: - -- what the user or project commits to -- what properties define correctness -- which examples and counterexamples disambiguate the intent -- which assumptions remain open -- which claims are verified, manually judged, or explicitly unresolved -- how generated artifacts might drift from the original intent - -This shift does not make planning irrelevant. It changes the source artifact that planning consumes. A plan becomes one projection from a richer i***ntent graph*** rather than the primary purpose of the spec. - -## 2. Progressive Checkability - -The cleanest product-facing phrase from the second excerpt is: - -> Make specs progressively checkable, not "formal" as a binary category. -> - -The useful ladder is: - -1. human-readable claim -2. concrete example -3. counterexample -4. regression test -5. runtime-checkable contract -6. state-machine rule -7. invariant -8. proof obligation -9. explicit unresolved ambiguity - -This keeps formal verification as one endpoint on a spectrum. The tool should emit the weakest sufficient artifact for the claim at hand. Some claims need only examples. Some deserve property tests, state-machine checks, runtime assertions, or Dafny / Lean / TLA+-style proof obligations. Some should remain qualitative, but they should be marked honestly rather than laundered into fake precision. - -## 3. Shared Claims as the Core Ontology - -The strongest modeling proposal so far is to factor requirements and acceptance criteria through a shared primitive, tentatively called `Property`. - -```tsx -type Property = { - id: PropertyId - description: string - shape: PropertyShape - predicate?: PredicateLocator -} - -type Requirement = { - id: RequirementId - commits: PropertyId[] -} - -type AcceptanceCriterion = { - id: CriterionId - observes: PropertyId[] - observationMode: ObservationMode -} -``` - -This resolves a persistent ambiguity in the current Brunch development spec: - -- a requirement is a normative commitment: the system shall satisfy these properties -- an acceptance criterion is an epistemic witness: this observation would show those properties hold -- an invariant is a property with state or transition shape -- an assumption is a claim whose status should be tied to probes or validation evidence - -Requirements and criteria therefore should not collapse into the same item, but they should stop being parallel prose containers. Their relationship should be referential and many-to-many, not inferred from paraphrase similarity. - -The ontology branch sharpens this further: a spec can be understood as a graph of typed claims. Top-level item kinds are not merely buckets; they are modalities of claim. - -``` -goal value or outcome claim -context descriptive claim -constraint boundary claim -assumption uncertainty claim -decision choice claim -requirement obligation claim -invariant preservation claim -criterion oracle claim -example concrete witness or disambiguator -``` - -This distinguishes two ontologies now in play: - -- **Brunch-internal development ontology**: the richer `ln-*`produced `SPEC.md` register, with assumptions, decisions, invariants, verification stance, oracle tiers, blind spots, and planning traceability. -- **Brunch-product elicitation ontology**: the product's currently captured user-spec ontology: goals, context, constraints, assumptions, decisions, requirements, and criteria. - -The branch argues that the product ontology likely needs two additions: - -- `invariant`: a property that must remain true across relevant states, transitions, versions, or executions. -- `example`: a concrete scenario, trace, input/output, edge case, or counterexample that illustrates or disambiguates intent. - -The useful distinction is: - -``` -Requirement: - What the system must do. - -Invariant: - What must never be broken. - -Criterion: - How we judge, test, review, or prove it. - -Example: - A concrete case that disambiguates or witnesses it. -``` - -`Decision` should become narrower, not disappear: - -> A decision is a chosen direction among plausible alternatives, with durable consequences for future design, implementation, or interpretation. -> - -That avoids treating every user answer as a decision. A decision captures the choice; an invariant captures the rule that must keep holding after the choice. - -`Constraint` should remain a top-level kind, but gain internal subtypes such as `non_goal`, `scope`, `technical`, `policy`, `resource`, `compatibility`, and `environmental`. A constraint restricts the acceptable solution space; an invariant states what must remain true as the system operates or evolves. - -`Context` remains necessary, but should have promotion rules: - -- if it must be true for success, consider requirement or invariant -- if it limits acceptable solutions, consider constraint -- if it may be false and matters, consider assumption -- if it chooses among alternatives, consider decision -- if it just helps interpretation, keep it as context - -## 4. Knowledge Edges as Intent Semantics - -The ontology branch treats `knowledge_edge` as a critical signal, not merely graph-view infrastructure. - -> A planning spec can be a list. An intent spec needs a graph. -> - -Item kinds say what claims exist. Edge kinds say how claims justify, constrain, depend on, refine, and verify one another. That reasoning topology is where intent becomes inspectable. - -The important shift: - -``` -Planning-spec edge: - Task B depends on Task A. - Feature Y implements Requirement X. - -Intent-spec edge: - Requirement R exists to satisfy Goal G. - Requirement R is constrained by Constraint C. - Requirement R assumes Assumption A. - Invariant I protects Decision D. - Criterion K verifies Requirement R. - Example E disambiguates Requirement R. - Counterexample CE rules out Interpretation P. -``` - -The current relation vocabulary already points in this direction with relations like `depends_on`, `derived_from`, `constrains`, `verifies`, and `refines`. The branch proposes organizing relation kinds into semantic families: - -| Family | Example relations | Purpose | -| ------------- | ------------------------------------------------------------ | --------------------------------------- | -| Justification | `derived_from`, `motivated_by`, `supports` | Explain why a claim exists | -| Dependency | `depends_on`, `assumes`, `requires` | Explain what must remain valid | -| Boundary | `constrains`, `excludes`, `rules_out` | Explain how one claim limits another | -| Refinement | `refines`, `specializes`, `decomposes` | Explain how claims become more specific | -| Verification | `verifies`, `illustrates`, `counterexample_for`, `tested_by` | Connect intent to evidence | - -Negative edges are especially important. Intent is often clarified by ruling out plausible interpretations: - -``` -Counterexample CE1: - "Rejected review item appears in export." - -CE1 violates Invariant I-review-authority. -Constraint C-no-fake-closure rules_out Requirement candidate "auto-export draft reviews". -``` - -Edges also need epistemic metadata so inferred relations do not silently become false dependencies: - -```tsx -type KnowledgeEdge = { - sourceId: KnowledgeItemId - targetId: KnowledgeItemId - relation: RelationKind - support: 'explicit' | 'strong_inference' | 'weak_candidate' - status: 'proposed' | 'accepted' | 'rejected' | 'stale' - provenanceTurnId?: TurnId - rationale?: string -} -``` - -Not every visible graph edge should drive cascade, staleness, export explanation, or criteria generation. Relation policy should say whether an edge is user-visible, cascade-participating, export-relevant, staleness-producing, or useful only as a low-confidence suggestion. - -For LLM collaboration, the most important practical change is to provide edge-local neighborhoods, not only grouped item lists: - -``` -R17: Each phase exposes an explicit kickoff/frontier/recovery/handoff affordance. - -Incoming: - motivated_by G2: avoid fake closure and stranded users - constrained_by C8: no generic task-planning surface - derived_from D94: phase progression is frontier-anchored - -Outgoing: - verified_by K13: open phases bottom-load one visible artifact - protected_by I24: stream projection/hydration stability - refined_by R18: open interview phases default to kickoff/frontier/generation/recovery -``` - -That is a stronger context object than "all goals, all constraints, all requirements." It lets the interviewer and observer reason about consequences, gaps, and drift. - -## 5. ~~[removed]~~ - -## 6. Elicitation as Ambiguity-Targeted Disambiguation - -The TiCoder-style lesson is that users are usually better at recognizing intent in concrete cases than authoring formal predicates. - -Brunch should ask high-yield questions where plausible interpretations diverge: - -- Does this example match your intent? -- Which candidate outcome is correct? -- Is this edge case inside or outside the commitment? -- Does this criterion intentionally omit part of the requirement? -- Which interpretation would count as a serious bug if implemented? - -The goal is not a larger questionnaire. The goal is to generate candidate interpretations, find their disagreement points, and ask only where the answer collapses meaningful ambiguity. - -Approved examples, rejected examples, and "not relevant" labels become durable spec artifacts. They are not merely conversational aids; they are regression seeds and evidence for the intent graph. - -## 7. Behavioral Pattern Elicitation - -The second excerpt adds an important product direction: do not elicit only freeform documents; elicit into reusable behavioral patterns. - -Candidate patterns include: - -| Pattern | User-facing question | Artifact shape | -| ------------- | ----------------------------------------------- | --------------------------------- | -| Workflow | What states can this object move between? | State machine | -| Ownership | Who may perform this action? | Authorization predicate | -| Containment | Can this item belong to more than one parent? | Uniqueness / membership invariant | -| Undo / redo | What happens to redo after a new action? | History / future invariant | -| Collaboration | What happens to stale or offline actions? | Rebase / conflict semantics | -| Deletion | What references must disappear or remain valid? | Referential-integrity rule | - -This gives Brunch an intermediate surface between prose and formal methods. The interviewer can detect or propose a behavioral pattern, ask pattern-specific questions, and then generate the weakest useful checkable artifact. - -## 8. Kernel Typology - -The kernel branch develops the behavioral-pattern idea into a more general interviewer architecture. - -> A kernel is a reusable family of questions that exposes one class of latent requirement and maps answers into progressively checkable artifacts. -> - -This is related to Midspiral's technical "kernel" concept, but shifted toward elicitation. A technical kernel is reusable state-management or proof machinery parameterized by a domain. An elicitation kernel is reusable question-and-artifact machinery parameterized by a user's feature. - -Kernels are not domains. "Kanban," "subscription billing," "document sharing," and "calendar scheduling" are domains. Each domain composes several kernels. - -Example: offline Kanban editing likely combines: - -- state and lifecycle: cards move through workflow states -- containment and topology: cards belong to columns and positions -- concurrency and collaboration: stale moves need merge / reject / rebase semantics -- resource accounting: WIP limits bound column capacity -- temporal history: undo, redo, or event ordering may matter -- derived data and views: column counts and filters must agree with source state - -The interviewer should therefore not ask every possible requirements question. It should infer likely kernels and ask diagnostic, contrastive questions for those kernels. - -### Kernel Families - -The v0.1 kernel ontology from the excerpt: - -| # | Kernel | Interview focus | Artifact shape | -| --- | --------------------------- | ------------------------------------------------------- | --------------------------------- | -| 1 | Identity & reference | What exists, how it is identified, what can point to it | Entity model, reference invariant | -| 2 | Containment & topology | Parent / child, membership, ordering, graph constraints | Tree, list, or graph invariant | -| 3 | Validation & normalization | Valid inputs, canonical forms, equivalence | Validator / parser contract | -| 4 | State & lifecycle | States, transitions, terminality | State machine | -| 5 | Temporal history | Undo, audit, monotonicity, expiration | History / timeline invariant | -| 6 | Optimization & preference | Best valid outcome, tie-breaking | Objective or ranking relation | -| 7 | Authority & capability | Who may do what, delegation, revocation | Authorization predicate | -| 8 | Concurrency & collaboration | Conflicts, stale actions, merge / rebase | Conflict-resolution semantics | -| 9 | Transactions & atomicity | All-or-nothing multi-object updates | Transaction invariant | -| 10 | Resource accounting | Balances, quotas, conservation, capacity | Conservation / bounds invariant | -| 11 | Derived data & views | Cache, index, projection consistency | View consistency invariant | -| 12 | Error & recovery | Retry, rollback, compensation, degraded mode | Failure / recovery contract | -| 13 | External effects | APIs, queues, clocks, webhooks, side effects | Boundary / adapter contract | -| 14 | Change & migration | Compatibility, legacy data, feature evolution | Migration / refinement invariant | -| 15 | Observability & evidence | Logs, provenance, explanations, auditability | Trace / audit invariant | - -This should be treated as a working ontology, not final truth. The test is whether each kernel produces a distinct class of high-value questions and emitted artifacts. - -### Super-Families - -The fifteen kernels can be grouped into five super-families: - -| Super-family | Kernels | Framing question | -| -------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------- | -| Structural correctness | Identity & reference; containment & topology; validation & normalization | What exists? | -| Behavioral correctness | State & lifecycle; temporal history; optimization & preference | What can happen? | -| Multi-actor correctness | Authority & capability; concurrency & collaboration | Who or what can act? | -| System correctness | Transactions; resource accounting; derived data & views; external effects; error & recovery | What must stay consistent operationally? | -| Evolution & accountability | Change & migration; observability & evidence | How does this survive time, change, and scrutiny? | - -This hierarchy is useful because kernels are "orthogonal-ish" but real features compose them. The interviewer can classify a feature at the super-family level first, then activate narrower kernels. - -### Contrastive Kernel Questions - -Kernel questions should usually be contrastive, not open-ended. - -Poor shape: - -``` -How should permissions work? -``` - -Better shape: - -``` -If Alice shares a folder with Bob, and then a document is added to that folder later, -should Bob automatically get access to the new document? - -A. Yes, permissions inherit dynamically. -B. No, sharing applies only to current contents. -C. It depends on the document type. -``` - -This generalizes the TiCoder move beyond tests. The interviewer generates cases where plausible interpretations diverge, then asks the user to classify them. - -### Kernel Cards - -Each kernel could become a reusable spec-interview module: - -``` -Kernel: Containment & Topology - -Detects: - parent / child relations, lists, folders, graphs, ordered collections - -Goal: - discover structural invariants - -Questions: - - Can an item have multiple parents? - - Can cycles exist? - - Does order matter? - - Are duplicates allowed? - - What happens to children when a parent is deleted? - -Artifacts: - - entity relationship diagram - - acyclicity invariant - - unique-parent invariant - - ordering invariant - - deletion cascade policy - -Proof obligations: - - add preserves topology - - move preserves topology - - delete preserves topology - - reorder preserves topology - -Example tests: - - moving item between parents removes it from old parent - - deleting parent does or does not delete children - - attempting to create cycle is rejected -``` - -The important product move is to build around question families and emitted artifacts, not formal-methods terminology. Formality is the compilation target, not the user interface. - -## 9. Specification Validation and Drift - -Both source conversations converge on the same bottleneck: - -> There is no oracle for spec correctness other than the user. -> - -Therefore Brunch's core problem is not only generating stronger artifacts. It is validating whether generated artifacts still capture the user's intent. - -Important drift cases: - -- an LLM silently changes a spec while trying to satisfy a proof or test -- an acceptance criterion observes a property no requirement commits to -- a requirement commits to a property no criterion observes -- an invariant predicate no longer matches its prose description -- a prompt, schema, fixture, UI label, or API type drifts from the canonical ontology - -Drift should be surfaced in human terms: - -``` -Original intent: - "Redo should behave like most apps." - -Generated behavior: - "Any new action after undo clears the future." - -Potential mismatch: - "Some apps support branching histories. Do you want linear history or branching history?" -``` - -This is more useful than exposing formal artifacts directly. It asks the user to validate meaning at the point where meaning could have changed. - -## 10. Brunch Development Methodology - -The first conversation eventually shifts from Brunch-the-product to the methodology used to build Brunch itself. - -The current `memory/SPEC.md` is already structured, but the structure is markdown-mediated. That creates cognitive strain for LLM contributors: - -- they must parse the whole document to make local changes -- cross-reference maintenance is textual and fragile -- retirement, supersession, and validation status require editorial discipline -- consistency is checked by rereading, not by querying - -The emerging direction is to make the development spec queryable and tool-operated, then render markdown as a view: - -``` -spec/ - properties.json - requirements.json - criteria.json - assumptions.json - decisions.json - predicates/ - validators/ -memory/SPEC.md # generated projection -``` - -Agent skills would move from document editing to structured operations: - -- query relevant spec slice -- add property -- link requirement to property -- attach criterion witness -- retire claim with rationale -- run spec validators -- generate human-readable projection - -This would make the richer ontology less burdensome. Complexity moves into schemas, validators, and tools; the LLM sees scoped projections and structural diffs. - -The alternate branch gives this a more operational shape: Brunch's development spec should become a small source-code-like registry, not a miniature runtime database. - -The current `SPEC.md` is doing many jobs at once: - -- human-readable product narrative -- agent-readable current truth -- decision / assumption register -- verification map -- glossary -- architecture model -- test coverage index -- working memory for coding agents - -That is powerful, but it forces every contributing LLM to repeatedly parse a large prose document, infer active vs retired concepts, preserve cross-section consistency, and perform global housekeeping while trying to make local code changes. - -The proposed development shape: - -``` -memory/spec/ - schema/ - record.schema.json - relation.schema.json - records/ - goals.yaml - context.yaml - constraints.yaml - assumptions.yaml - decisions.yaml - requirements.yaml - invariants.yaml - criteria.yaml - examples.yaml - terms.yaml - verification.yaml - generated/ - SPEC.md - AGENT_BRIEF.md - VERIFY_MAP.md - OPEN_RISKS.md - tools/ - check.ts - render.ts - slice.ts -``` - -The important view distinction: - -- **canonical**: small typed records, one per claim -- **rendered**: disposable generated markdown views for humans and agents - -The most important generated view may be `AGENT_BRIEF.md`: a compact, redundant file containing the product thesis, global non-negotiables, current architecture seams, active invariants, verification commands, and "for any change" rules. Most coding agents should not need the whole `SPEC.md`; they should need the brief plus a task-local slice. - -Task-local slices should be generated by tag and relation traversal: - -``` -Feature area: graph-view - -Relevant requirements: - REQ-... - -Relevant decisions: - DEC-128 - DEC-129 - -Relevant invariants: - INV-graph-projection-authority - INV-no-second-durable-model - -Relevant criteria: - CRIT-graph-structured-list - -Open assumptions: - A69 - A70 -``` - -This turns "read the whole spec again" into indexed retrieval by concern. - -### Tool vs Direct File Edits - -The branch separates canonical substrate from mutation interface. - -For development, a staged approach seems right: - -1. Agents may edit structured files directly while the model is changing, but a deterministic checker validates schema, IDs, relations, and generated views. -2. Common semantic mutations move behind a CLI/tool: add requirement, retire assumption, link criterion to invariant, regenerate views, produce task slice. -3. Direct edits remain possible for humans, but agents prefer tools and CI rejects invalid registry state. - -The contract: - -``` -Canonical records are editable files. -The tool is the preferred mutation interface. -The checker is the authority. -Generated markdown is never edited directly. -``` - -This avoids overbuilding too early while still moving housekeeping out of the LLM's context window. - -### Spec Checker - -The tool should behave less like a database and more like a compiler for spec records: - -``` -records -> validated graph -> rendered views / task slices / check reports -``` - -Possible commands: - -``` -spec check -spec render -spec slice --tag graph -spec list --kind invariant --status active -spec add --kind invariant -spec retire INV-024 --reason "superseded by INV-031" -spec link CRIT-012 verifies INV-024 -``` - -Possible checks: - -- no dangling relation targets -- no duplicate IDs -- every requirement has at least one criterion or explicit verification gap -- every criterion verifies at least one requirement or invariant -- every invariant has an oracle, or is marked manual / proof-candidate / gap -- every active decision has rationale and affected scope -- every assumption has validation approach or retirement condition -- no retired record appears in active generated views -- no forbidden legacy term appears outside glossary aliases - -The LLM should propose semantic changes, rationale, examples, and likely affected records. Deterministic tools should own ID uniqueness, schema validity, relation integrity, status transitions, coverage gaps, staleness reports, and generated views. - -## 11. Persistence and Interaction Model - -The persistence question is broader than file format. An intent spec is not produced by one linear conversation. It evolves through: - -- primary interview turns -- side conversations -- graph refinement -- revisit flows -- formalization passes -- validation probes -- agent-generated candidate interpretations -- user-labeled examples and counterexamples - -That suggests the durable model should preserve: - -- claim identity independent of the conversation turn that surfaced it -- provenance back to turns, examples, and user validations -- status transitions such as proposed, accepted, superseded, retired, open, validated, or falsified -- many-to-many links between commitments, observations, assumptions, and evidence -- projections for human review, implementation planning, test generation, and verification - -This aligns with Brunch's existing direction: chat view and graph view should be projections over shared specification truth, not separate durable models. - -### Turn Spine vs Patch Ledger - -A missing branch of the current capture concerns early-user feedback about how knowledge items are created and updated. The detailed proposal now lives in [Patch Ledger and Reconciliation](../../design/PATCH_LEDGER.md); this section keeps only the architectural implication for intent-spec evolution. - -One original Brunch assumption was that a single primary conversation would sit at the center of the product. The current architecture reflects that: durable conversational turns are the branch-bearing lineage spine, and knowledge items are extracted from answered turns or accepted review outputs. - -The intent-spec direction creates pressure against that assumption. Brunch is starting to look less like a linear guided interview and more like a flexible workbench for building an intent graph, where semantic changes can originate from many interaction surfaces: - -- users may add knowledge directly from graph view -- users may edit or split existing items -- side-chats may refine one node or neighborhood -- candidate specs may introduce a batch of claims -- examples and counterexamples may be added outside the original turn -- verification probes may update confidence or checkability -- downstream implementation feedback may revise upstream intent - -In that world, the chat turn is still valuable provenance, but it should no longer be the natural historical spine for all semantic change. - -``` -Turn spine: - history is organized by conversational sequence. - -Patch ledger: - history is organized by semantic mutations to the intent graph. -``` - -The split under discussion is: - -``` -chat / turn: - conversational provenance and replay - -intent graph: - current semantic truth - -patch: - semantic mutation history - -workflow state: - current product process state - -reconciliation_need: - semantic debt created when graph changes may affect existing truth -``` - -This is not a hybrid in the sense of two competing historical authorities. It is a separation of concerns: turns remain conversation history; patches become semantic history; workflow remains explicit process state; reconciliation becomes an agent-managed review flow for stale or contradictory graph truth. See [Multi-Chat Substrate](../../design/MULTI_CHAT.md) for the concrete first substrate slice, and [Patch Ledger and Reconciliation](../../design/PATCH_LEDGER.md) for later semantic mutation history, reconciliation ordering, and open schema questions. - -The alternate branch makes an important persistence distinction: - -> Use the same ontology concepts where possible, but do not force Brunch's development registry and Brunch's runtime product state into the same storage substrate. -> - -For Brunch's development workflow: - -``` -file-backed canonical records -+ CLI mutation helpers -+ deterministic checker -+ generated markdown views -+ task slices for agents -``` - -Files are appropriate because development memory should be diffable, reviewable, branchable, and easy for coding agents to inspect. - -For Brunch-the-product: - -``` -SQLite + Drizzle remain runtime truth. -Markdown / YAML / implementation briefs are projections or interchange bundles. -``` - -The running app has concerns that a file registry should not own: - -- multiple specifications -- turn lineage -- semantic patch history if graph edits become first-class -- streaming state -- observer capture status -- phase outcomes -- review turns and review versions -- graph edges -- route hydration -- resume / reload -- local `.brunch/` persistence - -Those are relational, interactive, and stateful. SQLite remains the right canonical runtime substrate. - -The product should distinguish: - -| Layer | Contents | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| Spec content | goals, context, constraints, assumptions, decisions, requirements, invariants, criteria, examples, terms | -| Operational metadata | specification id, workspace path, timestamps, phase status, frontier turn id, observer status, streaming state, review version, lifecycle bookkeeping | -| Relational structure | provenance, lineage, graph edges, patch history, review membership, accepted items, phase anchors, verification links | - -Relational and operational data are not "spec content," but they are part of specification state. Keeping those categories distinct prevents the persistence schema from leaking into the product ontology. - -A useful architecture split: - -``` -1. Storage model - SQLite tables optimized for persistence, queries, provenance, and resume. - -2. Domain model - Typed TypeScript objects representing specification truth: - KnowledgeItem, Requirement, Criterion, Invariant, Example, Relation, ReviewSet. - -3. Projection model - Markdown export, graph view, sidebar grouping, agent handoff, implementation brief. -``` - -Drizzle can own persistence shape, while TypeScript domain schemas and relation-policy registries own semantic shape. - -The convergence path is shared ontology with different adapters: - -``` -packages/spec-ontology/ - kinds.ts - relations.ts - schemas.ts - validators.ts - projectors.ts - -SQLite adapter: - runtime app state - -File adapter: - development registry, fixtures, exports - -Markdown projector: - human / agent-readable docs -``` - -The rule of thumb: - -``` -If humans and agents should review it in Git, use files. -If the running app needs to mutate it interactively and resume precisely, use SQLite. -``` - -The unifying principle is not files vs database. It is: - -> The LLM proposes semantic changes; deterministic systems own structure, integrity, and projection. -> - -## 12. Product Implications - -Near-term product implications: - -- treat unresolved ambiguity as a first-class output category -- add interviewer moves that generate disambiguating probes -- detect behavioral kernels and ask pattern-specific questions -- add `invariant` and `example` as likely product-ontology candidates -- treat `knowledge_edge` as intent semantics, not only graph display -- treat open-ended graph editing as needing chat containers and reconciliation needs first, then semantic history separate from turn history; see [Multi-Chat Substrate](../../design/MULTI_CHAT.md) and [Patch Ledger and Reconciliation](../../design/PATCH_LEDGER.md) -- preserve approved / rejected examples as durable evidence -- distinguish human-readable claims from checkable artifacts -- eventually tie requirements and criteria through shared property-like claims - -Near-term development-methodology implications: - -- prototype a structured spec store for Brunch's own planning artifacts before trying to migrate everything -- generate markdown from structured records rather than making markdown the only source -- add validators for orphan claims, unobserved commitments, stale assumptions, and ontology drift -- rewrite selected `ln-*` skills around scoped spec operations instead of whole-document parsing -- prototype named behavioral properties for the workflow/stream projection model -- provide LLM context as edge-local neighborhoods around active claims -- prototype a file-backed spec registry, renderer, checker, and task-slice generator for Brunch's own development workflow -- keep Brunch runtime persistence on SQLite / Drizzle while strengthening domain schemas and relation policies above the storage layer - -## Open Questions - -- What is the right granularity for `Property` records? -- Which claims should remain qualitative but explicitly observable-only? -- Should Brunch-the-product expose properties directly, or keep them as an internal normalization layer? -- Should `invariant` and `example` become durable top-level product kinds? -- What relation kinds need to participate in cascade and staleness, and which should remain display-only? -- How should weak inferred edges be reviewed without flooding users or agents? -- Which patch-ledger schema choices in [Patch Ledger and Reconciliation](../../design/PATCH_LEDGER.md) should be promoted after the [Multi-Chat Substrate](../../design/MULTI_CHAT.md) slice lands? -- Which behavioral kernels are common enough to deserve first-class elicitation support? -- Are the fifteen kernel families distinct enough in practice, or should some merge after transcript testing? -- What should a first kernel-card implementation include: detection signals, question templates, artifact schema, validators, or all of these? -- What is the smallest structured-store experiment that would reduce LLM housekeeping without destabilizing current planning flow? -- Should the development registry begin as YAML records, JSONL records, or markdown-embedded `spec-record` blocks? -- Which mutations deserve CLI commands first: add, link, retire, supersede, mark stale, render, slice, or check? -- What should belong in the generated `AGENT_BRIEF.md` versus task-local slices? -- How should user-labeled examples, counterexamples, and ambiguity probes appear in export? diff --git a/archive/docs/archive/design/REVISIT_MODULE.md b/archive/docs/archive/design/REVISIT_MODULE.md deleted file mode 100644 index dbb10208e..000000000 --- a/archive/docs/archive/design/REVISIT_MODULE.md +++ /dev/null @@ -1,88 +0,0 @@ -# Knowledge-Graph Revisit Module Design - -> Design exploration from 2026-04-12. Referenced historically by SPEC.md D80. -> Status: archived. The user-facing revisit/cascade goal remains live, but the side-chat V2/V3 framing and the `revisit_session` persistence shape are superseded by the chat + reconciliation-need substrate in `docs/design/MULTI_CHAT.md` and the later semantic mutation history in `docs/design/PATCH_LEDGER.md`. -> Canonicality: this is a historical module design note, not the live frontier authority. For what is true now and what should happen next, prefer `memory/SPEC.md` and `memory/PLAN.md`. - -## Shape - -State machine lifecycle reconstructed from DB state on each HTTP request. No in-memory state survives between requests. - -### Persisted state (new `revisit_session` table) - -```typescript -interface RevisitSession { - id: number - specificationId: number - status: 'planned' | 'active' | 'closing' | 'done' | 'aborted' - rootItemIds: number[] // items the user invalidated - affectedItemIds: number[] // cascade result - phasesToReopen: Phase[] // derived from affected items - anchorTurnId: number // highest primary-tree turn linked to affected items - threadRootTurnId: number | null // set when thread opens - createdAt: string - completedAt: string | null -} -``` - -### Projected state (read-only, reconstructed per request) - -```typescript -type RevisitState = - | { status: 'none' } - | { status: 'planned'; session: RevisitSession; preview: CascadePreview } - | { status: 'active'; session: RevisitSession; resolved: number[]; remaining: number[] } - | { status: 'closing'; session: RevisitSession } - | { status: 'done'; session: RevisitSession } -``` - -### Module boundary (5 functions) - -```typescript -/** Read-only: compute cascade without writing anything */ -function previewCascade(specificationId: number, itemIds: number[]): CascadePreview - -/** planned: write the session, mark items invalidated */ -function beginRevisit(specificationId: number, itemIds: number[]): RevisitSession - -/** active: open the secondary thread, reopen phases */ -function openRevisitThread(sessionId: number, anchorTurnId: number): RevisitSession - -/** active: mark one item resolved (called per-item as conversation progresses) */ -function resolveRevisitItem(sessionId: number, itemId: number, outcome: 'confirmed' | 'edited' | 'removed'): RevisitState - -/** closing → done: finalize when all items resolved */ -function completeRevisit(sessionId: number): RevisitSession -``` - -## What it hides - -- Graph traversal (BFS over all edge types, cycle detection) -- Which phases to reopen (derived from affected items' kind → phase mapping) -- Phase outcome supersession writes -- Review state reset for affected items -- Secondary thread turn creation with correct phase/ancestry -- Resolution completeness checking -- Anchor turn calculation (highest primary-turn provenance among affected items) - -## HTTP mapping - -| Step | Trigger | Writes | -|---|---|---| -| `previewCascade` | User selects items in edit mode | None (read-only) | -| `beginRevisit` | User confirms cascade | `revisit_session` + `turnKnowledgeItem(invalidated)` | -| `openRevisitThread` | Immediately after begin | Thread root turn + phase outcome supersession | -| `resolveRevisitItem` | Chat handler after each secondary turn | `turnKnowledgeItem(confirmed/edited)` per item | -| `completeRevisit` | When `remaining` reaches zero | Session status → done | - -## Design alternatives considered - -- **A (Minimal):** 2-method surface (`preview` + `invalidate`). Too coarse — doesn't model the interactive conversation lifecycle that spans many HTTP requests. -- **B (Event-driven):** Discrete event pipeline. Good auditability but ordering enforcement is runtime-only, not compile-time. Implementation style adopted (each step is stateless, state from DB). -- **C (Pure state machine):** In-memory stateful machine. Doesn't fit HTTP-per-request model without external persistence. Shape adopted, implementation adapted to DB-projected state. - -## Open questions - -- How does the secondary thread's chat endpoint differ from the primary? Same `/api/specifications/:id/chat` with a `threadId` param, or separate route? -- Does `resolveRevisitItem` happen automatically when the observer processes a secondary-thread turn, or does it require explicit user action? -- What happens if the user closes the browser mid-revisit? The session stays `active` in DB — next launch should resume. diff --git a/archive/docs/archive/design/WORKFLOW_OWNERSHIP.md b/archive/docs/archive/design/WORKFLOW_OWNERSHIP.md deleted file mode 100644 index ab2f10aab..000000000 --- a/archive/docs/archive/design/WORKFLOW_OWNERSHIP.md +++ /dev/null @@ -1,347 +0,0 @@ -# Workflow Ownership: projection vs transition - -> Clarification note for the workflow ownership frontier. -> Status: archived. The read/write ownership split has been implemented; this note is historical design context, not canonical product truth. -> For live authority, prefer `memory/SPEC.md` and `memory/PLAN.md`. -> Related design context: `docs/design/state-machines/README.md`. - -## Why this note exists - -Two planning items can sound blurrier than they are: - -- **Interview workflow transition extraction from `app.ts`** -- **Workflow projector extraction** - -Both concern the same architectural pressure: - -> **Where does the truth about interview/phase workflow live, and who is allowed to derive, advance, or interpret it?** - -They are related, but they target different layers: - -- **transition extraction** cleans up the **write path** -- **projector extraction** cleans up the **read path** - -A useful shorthand is: - -- **write-path workflow cleanup** -- **read-path workflow cleanup** - -## The four layers to keep distinct - -The easiest way to reduce the blur is to separate the workflow stack into four layers. - -### 1. Durable truth - -Persisted facts that are authoritative. - -Examples: -- turns on the active path -- phase outcomes -- accepted review outputs -- turn capture status -- any other workflow-bearing durable records - -This layer answers: - -> What facts are stored? - -### 2. Workflow projector - -A pure derivation layer that interprets durable truth into the current workflow/read-model state. - -Examples: -- current phase -- phase status, closeability, readiness -- current frontier turn -- whether the bottom artifact should be kickoff, frontier, recovery, handoff, or completion -- whether export is available - -This layer answers: - -> Given the stored facts, what workflow state is true right now? - -### 3. Workflow transition/orchestration - -The mutation layer that changes durable truth when the user or runtime takes an action. - -Examples: -- submit a reply -- accept or reject a closure proposal -- accept a requirements review -- create a successor frontier turn -- record a phase outcome -- advance to the next phase - -This layer answers: - -> When something happens, what durable facts should change? - -### 4. Transport and UI - -HTTP handlers, route loaders, query invalidation, and React rendering. - -Examples: -- `src/server/app.ts` -- route loaders and actions -- client-side query subscriptions -- rendered transcript/workspace surfaces - -This layer answers: - -> How do requests come in and how does the resulting state get delivered to the UI? - -## The actual concern - -The concern behind both planning items is that workflow semantics are still spread across multiple layers: - -- some are embedded in DB/read helpers -- some are embedded in `app.ts` -- some are implicit in route/query refresh behavior -- some are only legible through end-to-end reading instead of one named boundary - -That creates a few recurring problems: - -- the rules for workflow progression are hard to read in one place -- projection logic and persistence logic can blur together -- transport and domain logic can tangle -- tests have to prove behavior indirectly through larger surfaces than necessary -- later router/query ownership cleanup becomes harder because the underlying workflow boundary is not crisp - -## Item 1: transition extraction from `app.ts` - -## What it is - -This is the **write-path cleanup** item. - -It means moving workflow-changing logic out of the broad server transport layer and into a more explicit workflow/domain seam. - -Today, `src/server/app.ts` likely still does too much of the following in one place: - -- receive a mutation/request -- inspect current state -- decide which workflow rule applies -- create or update durable records -- create successor turns -- close phases -- advance into the next phase -- coordinate nearby side effects - -That makes `app.ts` more than transport; it becomes a partial workflow engine. - -## What it should become - -The target shape is: - -- `app.ts` handles request/response concerns -- a dedicated workflow layer decides the transition -- persistence writes the resulting durable changes -- the app returns the projected read model - -In other words, `app.ts` should call workflow logic, not *be* the workflow logic. - -## What it affects - -Primary files/seams: - -- `src/server/app.ts` -- likely parts of `src/server/core.ts` -- request handlers for reply submission / review acceptance / phase close / successor creation -- tests that currently must go through app-level endpoints to verify workflow rules - -## What this cleanup improves - -- makes workflow mutation rules easier to read and change -- reduces duplication between different endpoint paths -- makes edge cases easier to test in isolation -- keeps transport concerns separate from workflow semantics - -## Example questions this layer should answer cleanly - -- What happens when a closure proposal is rejected? -- What durable writes happen when requirements review is accepted? -- When does the next phase open? -- What writes must exist before the client can truthfully render the next state? - -## Item 2: workflow projector extraction - -## What it is - -This is the **read-path cleanup** item. - -It means extracting a pure workflow projection layer from DB/read helper code, especially around `getCurrentWorkflowState()` in `src/server/db.ts`. - -Today, a DB-facing seam likely loads durable rows and then partly interprets them in-place to compute things like: - -- current phase -- closeability/readiness -- current frontier -- whether a projected kickoff/recovery/handoff should appear -- export readiness - -That logic is workflow interpretation, not storage. - -## What it should become - -The target shape is: - -- DB layer loads a durable snapshot -- one projector derives workflow state from that snapshot -- routes, app handlers, and tests consume that same derived result - -Conceptually: - -```ts -type WorkflowSnapshot = { - // durable facts only -} - -function projectWorkflow(snapshot: WorkflowSnapshot): WorkflowState { - // pure derivation only -} -``` - -The important property is not the exact types; it is the ownership boundary: - -- snapshot assembly belongs near persistence -- workflow interpretation belongs in the projector - -## What it affects - -Primary files/seams: - -- `src/server/db.ts` -- read-model assembly helpers -- workflow-state helpers used by the client/API -- tests around hydration, resume, bottom-of-stream state, phase status, and export availability - -## What this cleanup improves - -- makes workflow interpretation legible as one named seam -- separates storage concerns from workflow derivation -- improves testability for resumed/seeded states -- prepares later router/query ownership cleanup by making the authoritative read model easier to name - -## Example questions this layer should answer cleanly - -- Given this durable snapshot, what is the current phase? -- Is the phase open or closed? -- What is the one truthful bottom artifact? -- Is this a kickoff, frontier, recovery, handoff, or completion state? -- Is export available yet? - -## Side-by-side distinction - -| Concern | Transition extraction | Projector extraction | -| --- | --- | --- | -| Main job | Clean up how workflow state changes | Clean up how workflow state is interpreted | -| Path | Write path | Read path | -| Typical style | Mutation/orchestration | Pure derivation/projection | -| Primary smell | `app.ts` acting like workflow engine | `db.ts`/helpers mixing storage with interpretation | -| Main outputs | Durable writes and next-step decisions | Derived workflow state/read model | -| Main payoff | Clearer workflow rules | Clearer workflow truth | - -## Why they belong to one family - -Even though they are distinct items, they belong to one conceptual family: - -> **workflow ownership cleanup** - -Both are trying to make these boundaries more legible: - -- what is durable truth? -- what is derived truth? -- who may change workflow state? -- who may interpret workflow state? -- what belongs in transport/UI, and what does not? - -That is why they often feel adjacent in planning discussions. - -## What they affect elsewhere - -These are not just internal cleanup items. They influence several visible or near-visible concerns. - -### Transcript fidelity and resume behavior - -If projection is fuzzy or duplicated, resumed states can land incorrectly: - -- wrong frontier -- missing handoff/completion -- stale recovery -- incorrect open/closed interpretation - -### Route/query ownership - -If workflow truth is not clearly projected in one place, route/query invalidation often becomes coarser than necessary: - -- over-refresh -- unnecessary churn -- unclear loader ownership -- UI correctness depending on broad invalidation instead of precise boundaries - -### Lifecycle correctness - -The current spec direction wants: - -- durable workflow truth to remain authoritative -- no second client workflow store -- route-independent ownership for ephemeral lifecycle concerns -- projected control cards rather than durable kickoff/recovery truth - -Clear projector and transition boundaries support that directly. - -### Future feature work - -The cleaner these ownership seams are, the easier later work becomes: - -- export/output readiness -- close-phase flows -- grounding/recovery behavior -- transcript trust for seeded/resumed states -- later revisit/cascade workflow - -## Current file-level mental map - -This is only a rough orientation map, not a binding code inventory. - -### Durable truth / persistence-adjacent -- `src/server/db.ts` -- `src/server/schema.ts` -- persisted turn / phase outcome / capture-status records - -### Projector-shaped logic -- `getCurrentWorkflowState()` and nearby read-model derivation in `src/server/db.ts` -- any helper that computes current phase/frontier/closeability/handoff from durable records - -### Transition-shaped logic -- request handlers in `src/server/app.ts` -- any code in `src/server/core.ts` or nearby modules that decides what successor write/phase progression should happen next - -### Transport/UI-shaped logic -- API endpoints in `src/server/app.ts` -- route loaders and actions -- `src/client/routes/specification/$id/_view/*` -- client refresh/invalidation behavior - -## How to think about them in planning - -If the names start to blur again, use these translations: - -- **Workflow projector extraction** = make the system better at answering - - “What workflow state is true right now?” -- **Transition extraction from `app.ts`** = make the system better at answering - - “When this action happens, what durable workflow changes should occur?” - -Or more briefly: - -- **projector = read truth** -- **transition = write truth** - -## Recommendation for roadmap framing - -These can remain separate frontier items if needed, but they should be thought of as one architectural theme: - -- **workflow ownership cleanup** - - read-path cleanup: projector extraction - - write-path cleanup: transition extraction - -That framing keeps them distinct without pretending they are unrelated. diff --git a/archive/docs/design/AGENT_MUTATION_SURFACE.md b/archive/docs/design/AGENT_MUTATION_SURFACE.md deleted file mode 100644 index f79e1b309..000000000 --- a/archive/docs/design/AGENT_MUTATION_SURFACE.md +++ /dev/null @@ -1,243 +0,0 @@ -# Agent Mutation Surface Audit - -Status: FE-698 design audit, 2026-05-07. - -## Purpose - -Requirement 42 and D143 establish a hard boundary: durable Brunch data mutations initiated by agents must enter through Brunch-owned handlers, not direct ORM access or harness-specific tool implementations. This document inventories the current mutation paths that are agent-originated or agent-adjacent, names the semantic operations behind them, and identifies holes before implementing an agent capability / mutation-surface registry. - -This is a boundary map, not a registry implementation. - -## Terms used here - -- **Agent-originated**: an LLM/tool loop chooses content or an action that causes a durable write. -- **Agent-adjacent**: a user action, route, or runtime step persists agent-produced artifacts or operations intended to become agent-addressable later. -- **Authority class**: - - `read_only`: no durable mutation. - - `provisional_artifact`: durable or replayed context that is not accepted graph truth. - - `proposal_only`: model/user proposes a change, but separate acceptance owns truth. - - `commit_truth`: writes durable semantic or workflow truth. - - `commit_process_debt`: writes obligations such as reconciliation needs. - - `runtime_replay`: writes replay/status artifacts tied to an existing durable unit. -- **Boundary quality**: - - `strong`: named application handler/transition owns validation and write semantics. - - `mixed`: some semantic grouping exists, but DB helpers remain exposed at agent/tool call sites. - - `thin`: route or agent code directly orchestrates DB helper calls. - - `missing`: projected capability has no handler yet. - -## Current mutation inventory - -| Area | Current entry points | Initiator | Tables touched | Semantic operation | Authority | Boundary quality | Notes | -| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Specification creation | `createNewSpecification()` in `src/server/core.ts`; `POST /api/specifications` in `src/server/app.ts`; `createSpecification()` in `src/server/db.ts` | user/system | `specification` | Create specification workspace record | `commit_truth` | `strong` | Not agent-originated today, but future CLI/TUI harnesses may need it. Keep as a product handler, not a raw DB tool. | -| Phase entry / projected start | `submitPhaseIntentWithRuntimeCompatibility()` via `POST /api/specifications/:id/phase-intent`; chat route `phase-entry` command via `applyChatRouteTransition()` | user/system, future harness | `turn`, `specification.active_turn_id` after finalization | Start or continue a workflow phase by creating a successor frontier turn | `commit_truth` | `strong` | Good candidate for a future mutation-surface contract because route code delegates to runtime/transition helpers. | -| Chat continuation / answering frontier | `applyChatRouteTransition()` in `src/server/chat-route-transition.ts`; `prepareTurn()`, `resolveTurn()`, `prepareSuccessorTurn()`, `finalizeTurn()` in `src/server/core.ts` | user message plus interviewer runtime | `turn`, `specification.active_turn_id`, possibly `phase_outcome` supersession | Resolve current turn, advance interview head, create next frontier | `commit_truth` | `strong` | This is the main workflow-write seam today. Future agents should call a handler with chat-command semantics, not `createTurn()` / `advanceHead()` directly. | -| Interviewer question persistence | AI SDK tool `ask_question` from `createAskQuestionTool()`; `persistStructuredQuestion()` in `src/server/interview.ts` | internal interviewer agent | `turn`, `option` | Populate prepared assistant turn with question, rationale, impact, options, and review metadata | `commit_truth` / `proposal_only` for review-set content until accepted | `mixed` | Agent tool execution directly calls persistence helper. Semantics are named, but this should become a Brunch-owned mutation handler before exposing interviewer-like tools to external harnesses. | -| Interviewer preface presentation | AI SDK tool `present_preface`; `materializeTurnArtifacts()` in `src/server/turn-artifacts.ts`; `updateTurn()` in `app.ts` on stream finish | internal interviewer agent | `turn.assistant_parts` | Persist provisional context/preface and activity artifacts for replay | `provisional_artifact` / `runtime_replay` | `mixed` | Tool itself returns success only; durable write happens later from response artifacts. Future contract should preserve the rule that prefaces do not directly mutate graph truth. | -| Phase closure proposal | AI SDK tool `propose_phase_closure`; `createPhaseOutcome()` in `src/server/interview.ts` | internal interviewer agent | `phase_outcome` | Propose closing grounding/design for user confirmation | `proposal_only` | `mixed` | Current tool writes proposed workflow state directly. Future mutation surface should expose `phase.proposeClosure`, with confirmation as a separate handler. | -| Phase closure confirmation | `applyChatRouteTransition()` confirm branch; `confirmPhaseOutcome()`, `finalizeTurn()` | user, future harness | `turn`, `phase_outcome`, `specification.active_turn_id` | Accept interviewer-proposed phase closure | `commit_truth` | `strong` | Already a coherent transition handler. Good model for future agent mutation handlers. | -| Forced phase closure | `applyChatRouteTransition()` force-close branch; `createConfirmedPhaseOutcome()` | user, future harness | `turn`, `phase_outcome`, `specification.active_turn_id` | Close phase without interviewer recommendation | `commit_truth` | `strong` | User-authority only today. External agents should not get this by default; if exposed, authority class must remain explicit. | -| Structured response submission | `submitTurnResponseTransition()` via `POST /turns/:turnId/response` | user, future harness | `option`, `specification.mode`, `turn`, possibly `knowledge_item`, `turn_knowledge_item`, `knowledge_edge`, `phase_outcome` | Persist selected options/free text, grounding mode, and accepted review decisions | `commit_truth` | `strong` | Good existing product handler. It also materializes review truth on accept, so future tools should not bypass it by writing accepted requirements/criteria directly. | -| Requirements/criteria review materialization | `materializeAcceptedRequirementsReviewSet()`, `materializeAcceptedCriteriaReviewSet()` from `submitTurnResponseTransition()` | user acceptance of agent-generated review set | `knowledge_item`, `turn_knowledge_item`, `knowledge_edge`, `phase_outcome` | Convert accepted review set into durable requirements/criteria and grounding edges | `commit_truth` | `strong` when reached through response transition; `thin` if called directly | The semantic operation is acceptance-gated materialization. Future agents may propose review sets but must not commit them without acceptance. | -| Observer capture | `runObserver()` via `ensureObserverCapture()` / `POST /observer-capture` and trailing runtime capture | internal observer agent | `knowledge_item`, `turn_knowledge_item`, `knowledge_edge` | Extract intent items and supported intent edges from validated turns | `commit_truth` for captured intent-graph truth | `mixed` | Agent runtime directly creates intent items and edges through DB helpers (`knowledge_item` / `knowledge_edge` today). This is the most important current agent-originated write surface to wrap before external harness access. | -| Observer result attachment / replay | `ensureObserverCapture()` in `src/server/app.ts`; observer result data parts on originating turn | internal observer runtime | `turn.assistant_parts` plus graph tables via `runObserver()` | Attach observer status/results to originating turn for replay | `runtime_replay` plus `commit_truth` | `mixed` | Needs separation between graph mutation handler and replay/status handler. Current endpoint dedupes runtime execution but not a future general mutation contract. | -| Intent item edit | `handlePatchKnowledgeItem()` in `src/server/edit-route.ts`; `updateKnowledgeItemContent()` | user graph edit today; future agent proposal/commit | `knowledge_item` | Edit accepted intent-item content after impact classification | `commit_truth` when soft; `proposal_only` when hard impact | `thin` | Route handler owns policy directly. Future agents must not call `updateKnowledgeItemContent()`; this should become a named mutation handler with reconciliation semantics. | -| Knowledge edge create/delete | `handleCreateKnowledgeEdge()`, `handleDeleteKnowledgeEdge()`; `addKnowledgeRelationship()`, `removeKnowledgeRelationship()` | user graph edit today; observer agent for create only | `knowledge_edge` | Add or remove semantic relationship | `commit_truth` | `thin` for route; `mixed` for observer | Edge writes need a single semantic handler that applies relation policy, provenance, support/status, and future reconciliation behavior. | -| Edge validation | `handleValidateKnowledgeEdge()` | user/UI, future agent/harness | none | Check relation policy before edge mutation | `read_only` | `strong enough` | Should become a read-only capability contract available to probes/harnesses. | -| Annotation create/delete | `handleCreateAnnotation()`, `handleDeleteAnnotation()` in `src/server/annotation-route.ts` | user side-chat/selection surface today; future agent notes possible | `annotation` | Attach or remove human annotation anchored to intent item/span | `commit_truth` but commentary, not intent-graph truth | `thin` | User-authored today. If agents can annotate, authority should likely be `proposal_only` or visibly agent-authored. | -| Side-chat response | `handleSideChatRequest()` in `src/server/side-chat-route.ts` | side-chat assistant agent | none durable today | Generate refinement discussion around pinned graph item | `read_only` / non-durable | `strong enough` | It does not persist chat messages today. Future multi-chat substrate will convert this into durable chat turns and likely graph proposals. | -| Workspace exploration tools | `src/server/tools/*` via interviewer tool set | internal interviewer agent | none durable directly | Read files, grep, find, list directory, optionally present preface | `read_only` plus provisional preface artifact | `strong enough` for read-only | These are harness-like tools already. They should be adapted from read-only capability contracts if exposed to CLI/TUI/Pi. | -| Scenario runner artifacts | `src/server/scenario-runner.ts` | developer/probe harness | none durable today | Capture rendered prompt/context/model/output placeholders | `read_only` / artifact outside product state | `strong enough` | Future artifact persistence should use a schema and remain outside product truth unless explicitly imported. | - -## Functional set vs semantic set - -Current code exposes many low-level DB helpers (`createTurn`, `updateTurn`, `advanceHead`, `createKnowledgeItem`, `addKnowledgeRelationship`, etc.). These are functional primitives, not agent-addressable operations. The mutation surface should instead expose semantic handlers such as: - -| Semantic operation | Current functional primitives | Notes for future handler | -| ----------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------- | -| `workflow.startPhase` | `prepareSuccessorTurn`, `createTurn` | Must check landing/runtime availability and active path. | -| `workflow.answerFrontier` | `resolveTurn`, `finalizeTurn`, `prepareSuccessorTurn` | Must preserve turn lineage and observer-capture scheduling. | -| `interviewer.persistQuestion` | `persistStructuredQuestion`, `createOption`, `updateTurn` | Agent-originated but production-internal today. | -| `workflow.proposePhaseClosure` | `createPhaseOutcome` | Proposal-only; separate from confirmation. | -| `workflow.confirmPhaseClosure` | `confirmPhaseOutcome`, `finalizeTurn` | User/harness authority gate. | -| `review.submitResponse` | `submitTurnResponseTransition` | Already a good handler; accepts review sets only through user action. | -| `observer.captureTurnIntent` | `runObserver`, `createKnowledgeItem`, relationship helpers | Should split model execution from intent-graph write application eventually. | -| `changeset.submit` with `intentItem.updateContent` | `handlePatchKnowledgeItem`, `updateKnowledgeItemContent` | Needs reconciliation/changeset-ledger integration before external agent writes. | -| `changeset.submit` with `intentEdge.create` / `intentEdge.delete` | edge route handlers, relationship helpers | Needs relation support/status/provenance semantics. | -| `annotation.create` / `annotation.delete` | annotation route handlers | Not core graph truth, but still durable state. | -| `workspace.read*` | `src/server/tools/*` | Read-only capability family; useful first adapter target. | - -## Holes and pressure points - -1. **Observer graph writes are agent-originated and still DB-helper-shaped.** `runObserver()` directly creates items and edges. Before external or scenario-driven agents can write graph truth, this should become an application handler that accepts validated observer output and applies graph mutations with provenance and relation policy. - -2. **Interviewer tools are already tools, but not Brunch capability contracts.** `ask_question` and `propose_phase_closure` are AI SDK tools whose `execute` functions write durable state. This is acceptable internally, but external harnesses should not copy those tool definitions; they should adapt Brunch-owned handlers. - -3. **Graph edit routes are product handlers but not agent-safe mutation contracts.** They validate IDs and relation support, but they do not yet create reconciliation needs, changeset history, support/status metadata, or agent provenance. They should be considered UI handlers awaiting migration into a mutation surface. - -4. **Review acceptance is a strong existing pattern.** `submitTurnResponseTransition()` shows the desired shape: one semantic handler validates a user action and materializes durable truth. Future proposal-generating agents should feed this kind of acceptance path rather than directly creating requirements/criteria. - -5. **Read-only tools are safe first registry candidates.** Workspace read/grep/find/list and relation validation can prove registry/adapters without mutation authority risk. - -6. **No durable side-chat substrate yet.** Side-chat is currently SSE-only for assistant output. Multi-chat will create new durable chat mutation needs; those should be designed through the same mutation surface rather than as side-chat-specific tools. - -7. **Scenario runner has no tool/capability inventory.** Probe artifacts should eventually record available capability ids and authority classes even when execution is not run, so prompt reviews can see what the agent was allowed to do. - -## Operation nomenclature - -The candidate capability registry should use product-operation names, not implementation-lineage names. Current functions such as `createAskQuestionTool`, `applyChatRouteTransition`, and `submitTurnResponseTransition` describe how the code got here: AI SDK tool creation, Express chat-route plumbing, or transition helper extraction. Canonical capability ids should instead name the durable product noun being acted on and the semantic verb being requested. - -### Canonical nouns - -Use these nouns for operation ids and handler names unless a later spec decision renames the underlying product entity: - -- `specification` — the workspace-scoped intent-spec container. -- `chat` — a durable conversation container below a specification once multi-chat lands. -- `turn` — a branch-bearing conversational lineage node. -- `phase` — workflow phase state and phase-outcome decisions. -- `intentItem` — a durable typed claim in the intent graph. Current storage is `knowledge_item`; new operation vocabulary should not inherit that table name. -- `intentEdge` — a durable semantic relation in the intent graph. Current storage is `knowledge_edge`. -- `reviewSet` — interviewer-generated requirements/criteria set awaiting user action. -- `annotation` — durable commentary anchored to an intent item/span. -- `changeset` — one semantic mutation bundle in the future changeset ledger. -- `change` — one atomic semantic mutation inside a changeset. -- `reconciliationNeed` — process debt saying existing truth may need renewed judgment. -- `workspace` — read-only project filesystem context. -- `scenario` — pre-UI prompt/context probe execution or artifact capture. - -Use `changeset` / `change` as canonical future schema and operation vocabulary. `patch` remains a historical design-doc synonym only. - -### Canonical verbs - -Use verbs by authority level: - -| Authority level | Preferred verbs | Notes | -| --- | --- | --- | -| Read-only | `get`, `list`, `query`, `render`, `validate` | No durable mutation. | -| Provisional/generated | `draft`, `propose`, `capture`, `render` | Produces candidate or replayable artifact, not accepted truth by itself. | -| User/handler submission | `submit` | Entry point for a caller request that may validate, route, or produce a proposal. | -| Durable transition | `apply`, `accept`, `reject`, `supersede`, `resolve`, `close`, `advance` | Changes durable product truth or process state. | -| Persistence primitive | `insert`, `update`, `delete` | Keep inside DB/repository helpers; do not expose as agent capability verbs. | - -Rule of thumb: agent-addressable operations should almost never be named `create`, `update`, or `delete`. Those are persistence verbs. Capability ids should name product intent: `turn.answer`, `phase.proposeClosure`, `reviewSet.accept`, `changeset.submit`. - -### Operation id grammar - -Use dotted ids: - -```text -. -``` - -Examples: - -```text -specification.create -specification.get -specification.export -chat.start -chat.submitMessage -turn.answer -turn.attachQuestion -turn.attachArtifact -phase.proposeClosure -phase.confirmClosure -phase.forceClose -reviewSet.submitResponse -reviewSet.accept -observer.captureTurnIntent -observer.applyCapture -intentGraph.query -intentGraph.validateEdge -changeset.submit -changeset.apply -changeset.reject -reconciliationNeed.list -reconciliationNeed.proposeResolution -reconciliationNeed.applyResolution -workspace.readFile -workspace.search -scenario.render -scenario.captureArtifact -``` - -Adapter-specific tool names may differ to satisfy AI SDK, Pi, CLI/TUI, or external-agent conventions, but those names are projections over canonical Brunch operation ids. - -### Changeset-centered graph mutation design - -Most future intent-graph mutations should not become separate top-level tools. Instead, they should become `change.kind` variants submitted through a small number of changeset operations: - -```text -changeset.submit -changeset.apply -changeset.reject -changeset.listPending -``` - -Candidate `change.kind` values: - -```text -intentItem.create -intentItem.updateContent -intentItem.retire -intentEdge.create -intentEdge.delete -annotation.create -annotation.delete -reconciliationNeed.create -reconciliationNeed.resolve -``` - -A future changeset payload should carry origin (`user`, `internal-agent`, `external-agent`), harness (`ui`, `cli`, `pi`, `scenario-runner`), provenance (`turnId`, `chatId`, or prior `changesetId`), purpose (`graph-edit`, `observer-capture`, `architect-proposal`, `reconciliation`), and one or more atomic changes. This lets architect proposals, graph edits, reconciliation resolutions, and external-agent edits share one semantic mutation entry point while preserving user/HITL acceptance where required. - -Conversational/workflow operations should remain explicit rather than being forced into changesets. `turn.answer`, `phase.proposeClosure`, and `reviewSet.accept` manipulate lineage, workflow, and replay state; their side effects may eventually create changesets, but the requested operation is still workflow/turn/review-domain behavior. - -### Current-to-target name map - -| Current name | Target operation vocabulary | -| --- | --- | -| `createAskQuestionTool` | `turn.attachQuestion` as the handler; AI SDK `ask_question` as an adapter tool. | -| `persistStructuredQuestion` | `turn.attachQuestion`. | -| `createProposePhaseClosureTool` | `phase.proposeClosure`. | -| `applyChatRouteTransition` | Split across `chat.submitMessage`, `turn.answer`, `phase.confirmClosure`, and `phase.forceClose`. | -| `submitTurnResponseTransition` | `turn.submitResponse`; review-specific branches become `reviewSet.submitResponse` / `reviewSet.accept`. | -| `materializeAcceptedRequirementsReviewSet` / `materializeAcceptedCriteriaReviewSet` | `reviewSet.accept`. | -| `runObserver` | Split into `observer.captureTurnIntent` for model execution and `observer.applyCapture` for durable graph writes. | -| `handlePatchKnowledgeItem` | `changeset.submit` / `changeset.apply` with `intentItem.updateContent`. | -| `handleCreateKnowledgeEdge` / `handleDeleteKnowledgeEdge` | `changeset.submit` / `changeset.apply` with `intentEdge.create` / `intentEdge.delete`. | -| `handleCreateAnnotation` / `handleDeleteAnnotation` | `annotation.create` / `annotation.delete`, or changeset variants if annotations join the ledger. | -| `createExplorationTools` | `workspace.*` read-only capabilities adapted as tools. | - -## Projected future capability holes - -| Future scenario | Needed capability contracts | Authority concerns | -| ------------------------------ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| CLI/TUI harness driving Brunch | create/list specs, start phase, answer frontier, read graph, maybe export | Must use workflow handlers; no ORM access. Mutations should be user-commanded or explicit. | -| Pi harness prompt probes | read graph/context packs, workspace read tools, scenario artifact capture | Keep Pi adapter read-only/proposal-only until mutation surface exists. | -| Web research probe | web search/fetch, attach provisional research preface, propose intent items/sources | Research output should be provisional until accepted/observed; avoid direct graph writes initially. | -| Behavioral kernels | read graph neighborhoods, propose disambiguating questions/examples/invariants | Proposal-only until ontology/checkability handlers exist. | -| Architect proposals | read graph, propose changesets, create reconciliation needs | Must wait for changeset-ledger/reconciliation semantics before committing truth. | -| Reconciliation review | list needs, propose resolution, apply accepted resolution | Requires process-debt handlers and user/HITL acceptance boundary. | -| External agent graph edit | edit intent item, add intent edge, retire intent item, create example/invariant | Needs mutation handlers with provenance, support/status, reconciliation, and changeset history. | - -## Recommended next slices - -1. **Agent capability registry skeleton** — Define stable ids, descriptions, input/output schemas, authority classes, and adapter-neutral metadata. Seed it with read-only capabilities only (`workspace.readFile`-style contracts if reused, relation validation, graph read projections) plus non-executable placeholders for mutating handlers discovered here. - -2. **Observer graph mutation handler extraction** — Split `runObserver()` into model execution and `applyObserverCaptureOutput()` so the graph write operation is named, testable, provenance-aware, and eventually reusable by scenario/harness adapters. - -3. **Interviewer tool handler extraction** — Move `persistStructuredQuestion()` / `createPhaseOutcome()` tool execution behind Brunch-owned handlers, then make AI SDK tools adapters over those handlers. - -4. **Graph edit mutation surface design** — Before exposing graph edit tools to agents, align intent-item / intent-edge edit handlers with reconciliation needs and changeset-ledger direction. - -5. **Scenario artifact capability inventory** — Extend no-provider probe artifacts to record which capability ids and authority classes were available for a run, without executing them yet. - -## Verification notes - -Code-search cross-checks used for this audit: - -- DB mutation helpers: `rg "export function (create|add|update|delete|set|link|record|advance|apply|insert|save|start|complete)" src/server src/shared`. -- ORM write calls: `rg "insert\\(|update\\(|delete\\(" src/server src/shared`. -- Agent/runtime seams: `runObserver`, `createAskQuestionTool`, `createProposePhaseClosureTool`, `applyChatRouteTransition`, `submitTurnResponseTransition`, side-chat and edit route handlers. - -The inventory should be refreshed after the multi-chat/reconciliation substrate lands, because chat containers and `reconciliation_need` rows will add new write families. diff --git a/archive/docs/design/CONTINUOUS_WORKSPACE_HYBRID.md b/archive/docs/design/CONTINUOUS_WORKSPACE_HYBRID.md deleted file mode 100644 index b821e4e8d..000000000 --- a/archive/docs/design/CONTINUOUS_WORKSPACE_HYBRID.md +++ /dev/null @@ -1,412 +0,0 @@ -# Continuous Workspace Hybrid Design - -> Design exploration from 2026-04-20. -> Status: **selected and shipped in FE-709** — hybrid continuous workspace with preserved phase addressability; deeper route collapse remains deferred. -> Canonicality: this is a focused design note for the interview workspace shape, not the live product authority. For what is true now and what should happen next, prefer `memory/SPEC.md` and `memory/PLAN.md`. - -## Why this note exists - -The current interview workspace behaves like one long four-phase conversation, but the center pane is still entered through four separate phase routes: - -- `/specification/$id/grounding` -- `/specification/$id/elicitation` -- `/specification/$id/requirements-review` -- `/specification/$id/acceptance-review` - -That route split buys deep-linking, gating, and some clean reset boundaries, but it also makes phase handoff feel more page-like than conversation-like. - -This note evaluates three shapes for a more continuous workspace and recommends the smallest hybrid that changes the user experience without prematurely deleting route affordances that still carry useful meaning. - -## Current constraints - -Any replacement needs to preserve these constraints from the current architecture: - -1. Durable workflow truth stays loader-derived and server-authored. -2. The app keeps one chat session per specification, not one `useChat` per rendered phase. -3. `SpecificationWorkspaceLayout` and `ViewLayout` remain the main loader/layout shells. -4. Graph view remains a sibling mode of chat view, selected via the `view` search param. -5. Workflow gating stays honest: future phases may be visible, but only the current reachable phase is actionable. -6. During migration, phase URLs should remain deep-linkable even if they stop being the primary rendering boundary. - -## Design A: Route-Alias Continuous View - -### Shape - -Keep the current route tree, but make each phase route render the same continuous workspace component instead of a phase-filtered workspace. - -The route still provides a focused phase key, but only as an initial scroll target / highlighted section. - -```typescript -function ContinuousWorkspaceView({ focusedPhase }: { focusedPhase: WorkflowPhase }) - -function useContinuousWorkspace(focusedPhase: WorkflowPhase): ContinuousWorkspaceModel -``` - -### Usage - -```typescript -function GroundingView() { - return -} - -function RequirementsReviewView() { - return -} -``` - -### What it hides - -- one shared `useChat` session -- scroll-to-section on route entry -- section highlighting and scroll-spy state -- continuous transcript projection across all phases - -### Trade-offs - -Pros: -- minimal route churn -- preserves all current URLs -- easy rollback - -Cons: -- route layer still pretends each phase is a separate screen -- active-phase focus can drift from route URL after scrolling -- keeps more compatibility surface than the product likely wants long-term - -## Design B: Workspace Controller With Phase Addressability - -### Shape - -Introduce one workspace-level controller that owns the chat session, hydrated transcript, per-phase section projection, and phase focus behavior. - -Routes remain, but they become addressability shims rather than the primary state partition. - -```typescript -type WorkspaceFocusSource = - | { kind: 'route'; phase: WorkflowPhase } - | { kind: 'scroll'; phase: WorkflowPhase } - | { kind: 'system'; phase: WorkflowPhase } - -interface WorkspaceSection { - phase: WorkflowPhase - title: string - status: WorkflowPhaseState['status'] - readiness: WorkflowPhaseState['readiness'] - isReachable: boolean - isFocused: boolean - turnCount: number - artifacts: WorkspaceStreamArtifact[] - bottomArtifact: InterviewControllerBottomArtifactState | null -} - -interface WorkspaceNavigation { - focusedPhase: WorkflowPhase - visiblePhase: WorkflowPhase - focusPhase: (phase: WorkflowPhase, options?: { replace?: boolean }) => void - scrollToPhase: (phase: WorkflowPhase) => void - focusNextReachablePhase: () => void -} - -interface WorkspaceChat { - messages: readonly BrunchUIMessage[] - status: ChatStatus - isLoading: boolean - submitText: (text: string) => void - confirmPhaseClosure: (phase: WorkflowPhase, turnId: number) => void - forcePhaseClosure: (phase: WorkflowPhase) => void -} - -interface ContinuousWorkspaceController { - specification: InterviewDurableSpecificationState['specification'] - workflow: InterviewDurableSpecificationState['workflow'] - sections: readonly WorkspaceSection[] - navigation: WorkspaceNavigation - chat: WorkspaceChat - captureStatusByTurnId: ReadonlyMap -} - -function useContinuousWorkspaceController(options: { - initialPhase: WorkflowPhase -}): ContinuousWorkspaceController -``` - -### Usage - -```typescript -function ContinuousWorkspaceView({ initialPhase }: { initialPhase: WorkflowPhase }) { - const workspace = useContinuousWorkspaceController({ initialPhase }) - - return ( - - } - transcript={ - - } - /> - ) -} -``` - -### What it hides - -- one `useChat` instance and one hydration pipeline -- phase-local selectors over one merged transcript -- route-to-section focus bridging -- close-phase behavior that scrolls/focuses instead of navigating as the primary effect -- suppression of duplicate auto-entry / auto-continue across rerenders -- stable capture status across section transitions - -### Trade-offs - -Pros: -- matches the product mental model of one workspace stream -- preserves deep links and migration safety -- keeps routing, data ownership, and workspace rendering as separate decisions -- creates one named client seam for future refinement - -Cons: -- introduces a new controller boundary that must carefully replace phase-local reset assumptions -- scroll-spy and scroll restoration need explicit rules -- some route compatibility code remains until the cutover is complete - -## Design C: Chart-Backed Workspace Supervisor - -### Shape - -Move the continuous workspace under a chart-backed runtime supervisor. The supervisor owns focus changes, visible section state, auto-entry/continue, close-to-next-phase motion, and section-level rendering states. The React layer mostly subscribes to chart state. - -```typescript -interface WorkspaceSupervisorSnapshot { - focusedPhase: WorkflowPhase - visiblePhase: WorkflowPhase - sections: readonly WorkspaceSection[] - canAutoFocusNextPhase: boolean -} - -function useWorkspaceSupervisor(options: { - initialPhase: WorkflowPhase - durableSpecification: InterviewDurableSpecificationState - chat: WorkspaceChatRuntime -}): { - snapshot: WorkspaceSupervisorSnapshot - send: (event: WorkspaceSupervisorEvent) => void -} -``` - -### Usage - -```typescript -const workspace = useWorkspaceSupervisor({ - initialPhase, - durableSpecification, - chat, -}) - -workspace.send({ type: 'SECTION_VISIBLE', phase: 'design' }) -workspace.send({ type: 'PHASE_CLOSED', phase: 'grounding' }) -``` - -### What it hides - -- all runtime focus transitions -- stale scroll events versus durable workflow truth -- one-shot auto behaviors -- transition legality around close, handoff, and recovery motion - -### Trade-offs - -Pros: -- strongest behavioral rigor -- easiest place to encode tricky scroll/focus rules without React effect sprawl -- aligns well with the existing state-machine thinking in `docs/design/state-machines/` - -Cons: -- too heavy for the first cut -- adds a second major client abstraction at the same time as the UX change -- risks solving orchestration elegance before proving the simpler product move - -## Comparison - -### Depth - -- Design A is shallowest. It changes the rendering shape but leaves route semantics half-retired. -- Design B is deepest for the likely need. Its public API is still moderate, but it hides the real complexity in one named workspace seam. -- Design C can be deepest eventually, but its extra machinery is hard to justify before the continuous workspace proves itself. - -### Ease of correct use - -- Design A is easy to adopt but easy to misuse because route focus and scroll focus can diverge without a clear owner. -- Design B makes the intended ownership legible: one workspace controller, many projected sections. -- Design C can be safest in theory, but only if the team wants to invest in a chart-owned client seam now. - -### General-purpose vs specialized - -- Design A is specialized for migration only. -- Design B is specialized for this workspace, which is a feature, not a bug. -- Design C is more general than needed for the current frontier. - -### Implementation efficiency - -- Design A has the lowest startup cost. -- Design B best reuses existing helpers like `projectWorkspaceStream`, `createInterviewControllerViewState`, and phase-order utilities while collapsing duplicate per-phase controller work. -- Design C would likely require the most new code and the largest test rewrite. - -## Recommended direction - -Choose **Design B**. - -More specifically: implement a **workspace-level continuous controller** while preserving **phase addressability** during migration. - -That means: - -1. The center pane becomes one continuous transcript with section dividers for grounding, elicitation, requirements, and acceptance criteria. -2. The sidebar becomes a scroll-spy / section-jump surface, but still respects current workflow reachability. -3. The current phase routes remain available, but they become aliases that focus a section instead of defining a separate rendering boundary. -4. The route tree and layout shells remain intact until the continuous workspace is proven. - -## Recommended module shape - -### Public API - -```typescript -interface ContinuousWorkspaceController { - specification: InterviewDurableSpecificationState['specification'] - workflow: InterviewDurableSpecificationState['workflow'] - sections: readonly WorkspaceSection[] - navigation: WorkspaceNavigation - chat: WorkspaceChat - captureStatusByTurnId: ReadonlyMap -} - -function useContinuousWorkspaceController(options: { - initialPhase: WorkflowPhase -}): ContinuousWorkspaceController -``` - -This is intentionally one main hook, not four phase-local hooks. - -### Recommended internal composition - -```typescript -function useContinuousWorkspaceController({ initialPhase }: { initialPhase: WorkflowPhase }) { - const { durableSpecification } = useInterviewDataAdapter(...) - const chatRuntime = useSpecificationChatRuntime(...) - const sections = projectWorkspaceSections(durableSpecification, chatRuntime) - const navigation = useWorkspacePhaseNavigation({ initialPhase, workflow: durableSpecification.workflow }) - - return { - specification: durableSpecification.specification, - workflow: durableSpecification.workflow, - sections, - navigation, - chat: chatRuntime.chat, - captureStatusByTurnId: chatRuntime.captureStatusByTurnId, - } -} -``` - -### Section projection helper - -```typescript -function projectWorkspaceSections( - durableSpecification: InterviewDurableSpecificationState, - runtime: WorkspaceChatRuntime, -): WorkspaceSection[] -``` - -Responsibilities: - -- derive each phase's turns from one durable turn list -- derive each phase's filtered messages from one merged message list -- project section-level artifacts via the existing workspace-stream projector logic -- keep exactly one actionable bottom artifact in the current reachable phase -- render closed and future phases as replay-only or locked sections - -### Navigation helper - -```typescript -function useWorkspacePhaseNavigation(options: { - initialPhase: WorkflowPhase - workflow: WorkflowState -}): WorkspaceNavigation -``` - -Responsibilities: - -- map route entry to initial section focus -- update focused phase as the user scrolls -- preserve workflow gating when users jump from the sidebar -- replace current close-phase route navigation with focus-next-phase behavior -- optionally update URL with `replace: true` instead of pushing history on every scroll change - -## What this boundary should hide - -The controller should hide these specific complexities from the view layer: - -- one `useChat` session and one hydration path -- phase-local replay compaction over a merged message stream -- auto-entry and auto-continue suppression rules -- pending close handling and next-phase focus -- stable `submittedTurnId` and capture-status tracking across section changes -- route alias compatibility during migration -- scroll-spy state versus durable workflow truth - -The controller should **not** hide or redefine: - -- durable workflow truth -- route loader ownership -- graph view selection -- the server's authority over phase status, readiness, or landing truth - -## Migration plan - -### Step 1: Preserve the route tree, replace the center-pane renderer - -- Keep the current phase routes. -- Swap `InterviewView phase=...` for one `ContinuousWorkspaceView initialPhase=...`. -- Keep graph view untouched. - -### Step 2: Extract workspace-level chat/runtime ownership - -- Replace `useInterviewController(phase)` with `useContinuousWorkspaceController({ initialPhase })`. -- Keep existing helper logic where possible instead of rewriting projection semantics from scratch. - -### Step 3: Convert the sidebar from route-active to section-active - -- Maintain route links initially. -- Add section highlighting driven by scroll position. -- Change click behavior from strict route switch to `focusPhase`, while preserving URL compatibility. - -### Step 4: Retire route-first assumptions - -- Remove automatic dependence on route remount for phase-local state reset. -- Rewrite tests that currently assert per-phase route code-splitting as the architectural truth. - -### Step 5: Decide whether route collapse is still worth doing - -- If the hybrid works well, phase routes can become redirects or search-param aliases. -- If not, the product still benefits from the continuous center pane without requiring full route collapse. - -## Non-goals for the first cut - -- no second durable workflow model on the client -- no general runtime operations ledger -- no graph-view architecture change -- no full chart-backed client supervisor yet -- no attempt to make all future phases simultaneously interactive - -## Open questions - -1. Should the canonical deep-link target after migration be a search param like `?phase=design` or should the legacy phase paths remain as stable aliases indefinitely? -2. Should scrolling into a closed phase update the focused phase in the sidebar, or should the sidebar continue to privilege the current reachable phase unless the user explicitly jumps? -3. Should close-phase completion auto-scroll immediately to the next phase section, or should it reveal the handoff card first and wait for explicit continue? -4. How much of the current center-pane sticky header remains phase-scoped once the center pane shows multiple sections at once? diff --git a/archive/docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md b/archive/docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md deleted file mode 100644 index 696acb376..000000000 --- a/archive/docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md +++ /dev/null @@ -1,351 +0,0 @@ -# Conversational Workspace Runtime — Umbrella Design - -> Status: **active synthesis** — reconciled with `memory/SPEC.md` / `memory/PLAN.md` on 2026-05-15 after Track 1 shipped in FE-709. -> -> Scope: the runtime-cluster architecture following FE-674/V3.1 and FE-709. This doc synthesizes [MULTI_CHAT.md](./MULTI_CHAT.md), [SIDE_CHAT.md](./SIDE_CHAT.md), [PATCH_LEDGER.md](./PATCH_LEDGER.md), and [CONTINUOUS_WORKSPACE_HYBRID.md](./CONTINUOUS_WORKSPACE_HYBRID.md) into the current **chat/turn-first** Conversational Workspace Runtime. -> -> Authority: this doc owns cross-subsystem synthesis. `memory/SPEC.md` owns product/architecture truth; `memory/PLAN.md` owns frontier sequencing. Sibling docs remain subsystem/source references for shipped substrate details, UI history, algorithms, and design pressure. - -## 1. Purpose and positioning - -The **Conversational Workspace Runtime** is the architectural umbrella for turning the shipped continuous workspace shell into a durable multi-chat workspace: - -1. The workspace hosts a primary interview chat plus inline/collapsible **secondary chats** for side, reconciliation, QA, and strategy work. -2. Existing `chat` + `turn` persistence is the near-term runtime primitive. A schema-level `thread` table is explicitly deferred until chat/turn proves insufficient. -3. Reconciliation, proposal turns, and future agent-mediated work stay conversationally visible while semantic mutations remain server-authoritative through changesets. -4. Prompt context is **transcript-first**: extra graph/workspace context enters the transcript as explicit context snapshot artifacts stored on turns. Graph-item handles track explicit anchors/mentions, but freshness waits for real item versions from the changeset ledger; context is not a hidden persisted context-spec table. - -### What this doc is not - -- Not an implementation plan. The sub-tracks in §5 enter `/ln-plan` as frontier items. -- Not a UX spec. It names runtime ownership and cutover targets, not final visual design. -- Not a new authority for product ontology, changeset semantics, or prompt/context pack policy; those live in SPEC/PLAN and the relevant sibling docs. - -### Relationship to sibling docs - -| Sibling doc | Role going forward | -| --- | --- | -| [MULTI_CHAT.md](./MULTI_CHAT.md) | Shipped substrate reference for `chat`, chat-owned turns, primary-chat transition invariants, and `reconciliation_need`. Future runtime work builds on that substrate rather than adding schema-level threads by default. | -| [SIDE_CHAT.md](./SIDE_CHAT.md) | User-surface history and V1–V3.1 behavior. Its V4 persistent-history direction is superseded by durable secondary chats rendered inline in the workspace. | -| [PATCH_LEDGER.md](./PATCH_LEDGER.md) | Historical design pressure for semantic mutation history and reconciliation ordering. Future-facing vocabulary is `changeset` / `change`; target-ordering mechanics remain useful. | -| [CONTINUOUS_WORKSPACE_HYBRID.md](./CONTINUOUS_WORKSPACE_HYBRID.md) | Source note for the shipped Track 1 workspace shell and deferred route-collapse question. Runtime work builds on FE-709 rather than re-deciding the shell. | -| [INTENT_GRAPH_SEMANTICS.md](./INTENT_GRAPH_SEMANTICS.md) | Authority for relation-policy directionality, endpoint-relative labels, and neighborhood snapshot modes used by context provision and reconciliation impact. | -| [SUBSTRATE_STRANGLER_COORDINATION.md](./SUBSTRATE_STRANGLER_COORDINATION.md) | Coordination forecast for what backend/server functions and capability contracts frontend work should expect as FE-700/FE-701/Track 5 land. | - -### Supersession map - -| Claim type | Current authority | Superseded / historical | -| --- | --- | --- | -| Runtime concept and cross-track direction | This document + SPEC/PLAN | Reading MULTI_CHAT / SIDE_CHAT / PATCH_LEDGER as independent future roadmaps. | -| Secondary conversation persistence | `chat` + `turn` substrate, with secondary chats as product/runtime usage | Treating a `thread` table, `turn.thread_id`, or `thread_context_item` as accepted near-term schema. | -| Side-chat user surface | Inline/collapsible secondary chats in the workspace | `SideChatPopover`, top-bar staged patch list, and standalone Pending review as long-term surfaces. | -| Semantic mutation history | Changeset/change ledger | New durable schema or operation names using `patch` / `patch_change`. | -| Prompt context | Transcript-first snapshots stored as turn artifacts; item handles are lightweight anchor/mention references refreshed only with real item versions | Hidden persisted context-spec records as the default context authority; temporary content fingerprints as a durable refresh oracle. | -| Reconciliation vs graph review | Reconciliation needs are process debt from known disturbances; graph-review findings are critique artifacts | Using `reconciliation_need` as the table for all graph quality findings. | -| Agent mutation authority | Brunch-owned capability/handler contracts | Agents writing directly through ORM helpers or harness-specific route wrappers. | - -Live open questions are now narrower: secondary-chat lifecycle/rendering shape over chat/turn, reconciliation chat lifecycle, direct-edit chat-opening UX, `#` mention disambiguation, context-handle storage/expiry, compact snapshot serialization, async classifier scheduling, endpoint-relative relation-label UI affordances, and migration of existing client patch terminology. - -## 2. The shift, at a glance - -```mermaid -flowchart LR - subgraph Previous - S1[spec route] --> SLV[structured-list view] - S1 --> GV[graph view] - SC1[side-chat popover
anchored per item] -.-> SLV - PR1[Pending review section
flat list] -.-> SC1 - IV1[interview chat
only visible chat] --> SLV - end - - subgraph Target - WS[continuous workspace shell] --> PS[primary interview chat] - WS --> SS[inline secondary chats] - SS --> SIDE[side chat
item anchored] - SS --> REC[reconciliation chat
target grouped] - SS --> QA[QA chat
explicit context] - SS --> STR[strategy chat
proposal turns] - SLV2[structured-list view] --> WS - GV2[graph view] --> WS - end -``` - -**What changes for the user** - -- One workspace surface can show the primary interview and secondary chats inline, usually collapsed until relevant. -- Reconciliation becomes an in-stream secondary chat, not a separate review section. -- The side-chat popover and Pending review section retire after parity exists over durable secondary chats. -- Direct edits stay fast-path. Hard-impact edits create reconciliation needs and may focus a side/reconciliation chat depending on context. -- Auto-confirmed reconciliation never surfaces. Only `auto-edit` and `substantive` need user attention. - -**What changes for the substrate** - -- The `chat` table stays the durable conversational primitive; turns remain chat-owned. -- Secondary chat kinds (`side`, `reconciliation`, `qa`, `strategy`) are represented through chat metadata / strategy state over chat+turn unless a later RFC proves a `thread` table is necessary. -- Changeset/change records become the semantic mutation spine; existing client “patch” state is transitional. -- Context provision becomes explicit transcript content: context snapshots are inserted into turns, and context handles track explicitly referenced graph items. Stale-handle refresh waits for changeset-backed item versions rather than a temporary content fingerprint. - -## 3. Architecture layers - -### 3.1 Workspace shell — Track 1, shipped - -The spec route renders a continuous workspace: realized phase sections, sidebar navigation, scroll/focus behavior, primary chat, projected controls, graph/sidebar affordances, and future inline secondary chats. This shipped in FE-709 as `continuous-workspace`. - -The shell is the structural prerequisite for absorbing side-chat and pending-review surfaces. Runtime work should build on the shipped hybrid shell and preserve phase addressability, single actionable frontier semantics, and workspace/read-model authority. - -### 3.2 Chat runtime — inline secondary chats over chat/turn - -Track 2 implements inline/collapsible secondary chats using existing `chat` + `turn` persistence. - -**Primitives** - -- `chat` — durable conversation container inside one specification. One primary interview chat exists; secondary chats are additional chat rows for side, reconciliation, QA, or strategy work. -- `turn` — chat-owned conversational event or assistant/system-first proposal/kickoff. Each active/resumable chat has at most one open assistant/system-first frontier turn. -- `chat.kind` / chat-local strategy metadata — distinguishes `side`, `reconciliation`, `qa`, and `strategy` behavior without creating semantic truth. -- **Turn-zero** — an assistant/system kickoff turn that seeds a secondary chat with explicit context snapshots and options before the user responds. The first Track 2 version can render snapshots supplied by server fixtures/builders without owning the full Track 5 refresh lifecycle. - -**Explicit deferral** - -Do not add a `thread` table, `turn.thread_id`, or schema-level thread hierarchy in Track 2. The follow-up question is not “which thread schema?” but “what, if anything, can chat/turn not express after secondary-chat rendering, one-open-frontier semantics, strategy state, and context snapshots are implemented?” - -**Cutover targets** - -- Retire `SideChatPopover` as the long-term side-chat surface after durable inline secondary chats reach parity. -- Retire the transient staged-patches strip as a semantic source of truth. In-flight proposals may remain turn artifacts, but accepted mutations flow through changesets. -- Preserve existing primary interview behavior during the transition. - -### 3.3 Reconciliation runtime — in-stream secondary chat - -Track 3 absorbs reconciliation into the workspace as a target-grouped secondary chat. - -**Trigger model** - -- **Async by default** — when `reconciliation_need` rows enter the queue, a background runner/classifier processes unclassified rows. Auto-confirmed rows resolve invisibly. Auto-edit rows queue one-click suggestions. Substantive rows accumulate for judgment. -- **User trigger** — “Reconcile Now” materializes or refocuses the reconciliation chat so the user can batch-review, retry, or force classification. - -**Chat shape** - -A reconciliation chat is a durable secondary `chat`, grouped by target and sorted upstream-first using the PATCH_LEDGER target-ordering pressure. Classifier labels (`auto-confirm`, `auto-edit`, `substantive`) are metadata on needs inside target groups, not the primary grouping axis. - -The reconciliation chat can host a conversation about substantive needs, but it does not own semantic truth. Accepted resolutions route through Brunch-owned handlers and, once Track 4 lands, durable changesets. - -**Surfacing rules** - -- Auto-confirmed rows do not surface. -- Open non-auto-confirmed needs may show subtle badges on structured-list / graph items. -- The workspace shell may show an active reconciliation count and Reconcile Now affordance without stealing focus. - -### 3.4 Changeset ledger — semantic mutation spine - -Track 4 introduces durable semantic mutation history. - -**Vocabulary** - -- `changeset` — one coherent submitted semantic mutation bundle. -- `change` — one atomic mutation inside a changeset. -- “Patch” is historical/transitional. It may remain in legacy client code until migrated, but it is not target vocabulary. - -**Attribution** - -- Every change belongs to one changeset. -- Every changeset records provenance: originating turn/chat where applicable, action source, actor, and base semantic state. -- `reconciliation_need.caused_by_changeset_id` replaces/connects the historical patch placeholder. -- Proposal turns are not mutations until accepted. - -**Direct edits** - -A direct graph/list edit writes a changeset and then opens/deduplicates reconciliation needs according to relation policy. The UX may focus an item-anchored side chat, append to a reconciliation chat, or remain in-place; that is a Track 3/4 interaction decision, not a semantic-history decision. - -### 3.5 Context provision — transcript-first snapshots and handles - -Track 5 provides prompt context for primary and secondary chats without a hidden persisted context-spec table. - -**Model** - -- A chat primarily uses its own transcript as prompt context. -- Extra graph/workspace context is inserted into the transcript as explicit **context snapshot** artifacts. -- A **context handle** records that a chat is tracking explicit graph subjects across turns, usually because a chat was anchored on an item or the user mentioned one with `#`. Before new assistant turns, stale handles can trigger fresh snapshots when the subject's changeset-backed item version has advanced. Do not bless temporary content fingerprints as the durable freshness oracle. -- Snapshots are historical: they do not mutate when source truth changes. - -**Turn-zero** - -Secondary chats begin with a kickoff turn that inserts kind-appropriate snapshots and offers options. Track 2 can create/render turn-zero artifacts before Track 5 fully lands, but the snapshots should already follow the transcript-first artifact model. Example defaults: - -- `side` — anchor item snapshot plus relation-policy-rendered neighborhood and open needs against the anchor. -- `reconciliation` — target group snapshots plus relevant needs and relation-policy context. -- `qa` — user-selected / `#`-mentioned items and enough graph metadata to answer safely. -- `strategy` — current phase/workflow state plus the fixed premise and proposal constraints. - -**Snapshot builder family** - -The server-side context layer should expose reusable builders; frontend runtime code should consume their artifacts rather than reconstruct graph meaning locally: - -| Builder shape | Purpose | -| --- | --- | -| `buildIntentItemContextSnapshot({ specificationId, itemIds })` | Explicit item inclusion and `#` mention context. | -| `buildIntentNeighborhoodContextSnapshot({ specificationId, anchorItemIds, mode })` | Side-chat, QA, edit-impact, and reconciliation context around anchors. Initial modes: `immediate`, `dependencies`, `dependents`, `evidence`, `reconciliation`. | -| `buildEconomicIntentGraphContextSnapshot({ specificationId, budget })` | Compact whole-graph briefing for unanchored chats in existing specifications. | -| `buildHistoricalIntentNeighborhoodSnapshot({ itemId, basis })` | Later, once changesets can identify original-capture and last-update surroundings. | -| `resolveIntentItemReferences({ specificationId, refs })` | Server-owned `#` reference-code/name resolution scoped to one specification. | - -Neighborhood labels and buckets (`dependencies`, `dependents`, `evidence`, etc.) come from relation policy endpoint-relative labels. They are not string reversals of `knowledge_edge.from_item_id` / `to_item_id`. - -**`#` mention** - -A mention such as `#AS-12` resolves against intent items through a server-owned resolver scoped to the specification. Resolution inserts a context snapshot and, once real item versions exist, activates or refreshes a handle for the mentioned subject. Ambiguity should produce an explicit disambiguation/recovery artifact rather than silently omitting context. Revocation, expiry, and storage shape are Track 5 design details, but the transcript remains the durable replay surface. - -**Serialization** - -Context-pack builders own compact rendering. TOON or another compact graph serializer may format inserted snapshots, but the serializer is an implementation choice, not an authority boundary. - -## 4. Cross-cutting decisions - -### 4.1 Vocabulary - -- `chat` — durable conversation container inside a specification. -- `secondary chat` — non-primary chat rendered inline/collapsible in the workspace; product/runtime use of `chat`, not a separate thread table. -- `turn-zero` — kickoff turn that seeds a secondary chat. -- `context snapshot` — explicit turn artifact recording inserted graph/workspace context at a point in time. -- `neighborhood snapshot` — context snapshot centered on one or more intent items, with relation-policy-selected modes such as immediate, dependencies, dependents/impact, evidence, reconciliation, and later historical. -- `context handle` — lightweight active reference to explicit graph subjects, causing future freshness checks and new snapshots only when changeset-backed item versions advance. -- `changeset` / `change` — durable semantic mutation vocabulary. -- “Thread” — historical or generic UI language only in this doc. It is not target schema vocabulary. -- “Patch” — historical/transitional client vocabulary only. - -### 4.2 What never surfaces to the user - -- Auto-confirmed reconciliation rows. -- Successful classifier runs that produce no user-relevant work. -- Changeset bookkeeping as a separate user-facing object when the resulting semantic state is enough. - -### 4.3 What surfaces subtly - -- Open non-auto-confirmed reconciliation needs against an item. -- Active reconciliation chat count / Reconcile Now affordance. -- Failed classifier rows as recoverable state. - -### 4.4 What surfaces actively - -- Substantive reconciliation needs with judgment affordances. -- Hard-impact direct edits when user attention is needed. -- Assistant/system-first proposal turns in strategy, graph-review, or reconciliation chats. - -## 5. Umbrella structure — sub-tracks - -```text -Track 1: Workspace shell — shipped FE-709 - ├─ Cumulative phase sections - ├─ Sidebar section-active behavior - └─ Structured-list and graph views as workspace-aware peers - -Track 2: Chat runtime — inline secondary chats - ├─ Represent side/reconciliation/qa/strategy chats over chat+turn - ├─ Render secondary chats inline/collapsible in the workspace - ├─ Preserve one open assistant/system-first frontier turn per active chat - ├─ Seed secondary chats with turn-zero kickoff + context snapshots - └─ Retire SideChatPopover after parity - -Track 3: Reconciliation runtime - ├─ Target-grouped reconciliation chat - ├─ Async-by-default classifier scheduling - ├─ Reconcile Now trigger in workspace shell - ├─ Retire standalone PendingReviewSection after parity - └─ Auto-edit one-click apply + substantive judgment affordances - -Track 4: Changeset ledger - ├─ changeset / change tables and mutation handlers - ├─ latest/base changeset identity for proposal staleness - ├─ reconciliation_need.caused_by_changeset_id wiring - ├─ Direct-edit and agent-resolution paths write changesets - └─ Migrate existing client patch vocabulary/state - -Track 5: Chat context provision - ├─ Context snapshot turn artifacts - ├─ Active graph-item handles and stale-handle refresh after real item versions exist - ├─ # mention parser/resolver/disambiguation - ├─ Item, neighborhood-mode, economic graph, and later historical snapshot builders - ├─ Turn-zero prompt assembly per chat kind/strategy - └─ Structured assertions + selected golden renderings for snapshots - -Dependencies - Track 1 (shell) —enables→ Track 2 (secondary chats) - Track 2 —enables→ Track 3 (in-stream reconciliation) - Track 2 —enables→ Track 5 (turn-zero, mentions, handles) - Track 4 (changeset) —parallel with→ Track 2 - Track 4 —enables→ richer attribution in Track 3 - Track 4 —enables→ real item versions and historical neighborhoods in Track 5 -``` - -**Why this order** - -- The shell came first because secondary chats need a stable host. -- The chat runtime is the unblocker for reconciliation absorption and transcript-first context provision. -- The changeset ledger can run in parallel because semantic history is independent of inline rendering. -- Context provision and reconciliation in-stream parallelize once Track 2 settles the initiating chat/anchor shape. - -## 6. Cross-document audit - -| Parallel design | Implication for the runtime cluster | -| --- | --- | -| [INTENT_GRAPH_SEMANTICS.md](./INTENT_GRAPH_SEMANTICS.md) | Reconciliation, direct-edit cascade, and context snapshots must consult relation-policy directionality, endpoint-relative labels, and edge support/status. Runtime code cannot infer affected endpoints or dependency/dependent labels from raw edge direction. | -| [SPEC_EVOLUTION_STRATEGIES.md](./SPEC_EVOLUTION_STRATEGIES.md) | Strategy is chat-local process state. Scenario options, graph-review findings, and reconciliation suggestions are proposal turns until accepted; accepted bundles become coherent changesets. | -| [AGENT_MUTATION_SURFACE.md](./AGENT_MUTATION_SURFACE.md) | Agent-originated writes enter through Brunch-owned capability/handler contracts, not direct ORM or route-wrapper mutation authority. | -| [SUBSTRATE_STRANGLER_COORDINATION.md](./SUBSTRATE_STRANGLER_COORDINATION.md) | Existing frontend REST/SSE contracts remain stable while backend work produces shared handlers, relation-policy functions, context snapshot builders, and capability tools. | -| Prompt/context substrate (SPEC D139/D140/D154, A84/A85/A95) | Chat context provision consumes prompt/context-pack policy. Snapshots are built by context-pack builders and stored on turns; handles organize refresh once changeset-backed item versions exist. | -| [BEHAVIORAL_KERNELS.md](./BEHAVIORAL_KERNELS.md) | Kernel-driven questions produce typed intent artifacts; the runtime provides chat/context affordances but not a separate artifact ontology. | -| [ln-skills/EVOLUTION.md](./ln-skills/EVOLUTION.md) | Dev-layer file-backed registry ideas are separate from product runtime persistence. | - -Audit result: the runtime is coherent when `chat` is conversational process, chat-local strategy remains turn/proposal state, prompt/context packs assemble context snapshots, relation policy owns endpoint-relative labels and impact direction, changesets own semantic mutation history and item versions, reconciliation needs represent process debt, and graph review remains a separate quality oracle. - -## 7. Out of scope / explicit deferrals - -- Pixel-level secondary-chat UX and designer consultation. -- Demo-bound prioritization; PLAN sequencing owns frontier order. -- Schema-level `thread` tables or `turn.thread_id`; deferred until chat/turn insufficiency is proven. -- Hidden persisted context-spec tables; deferred unless transcript-first snapshots/handles fail. -- Temporary content/edge fingerprints as the durable context-handle freshness oracle; handle freshness waits for changeset-backed item versions. -- Continuous-workspace route collapse; FE-709 hybrid shell remains the host. -- Architect/generator loop; horizon until changesets and reconciliation surfaces stabilize. -- Provider setup, gitignore assist, and productized web research; separate PLAN frontiers. - -## 8. Open questions - -- **Secondary-chat lifecycle** — when to create, refocus, collapse, archive, or resume side/reconciliation/qa/strategy chats. -- **Direct-edit attention UX** — when hard-impact direct edits focus an item side chat, a reconciliation chat, both, or neither. -- **Context-handle storage and expiry** — whether handles are explicit rows, turn-derived state, or both; how revocation and expiry are represented once real item versions exist. -- **`#` mention disambiguation** — reference-code/name matching and ambiguous-result UX. -- **Compact snapshot serialization** — TOON vs a minimal internal serializer; structured assertions plus selected golden renderings are the oracle, not exact prose everywhere. -- **Async-classifier scheduling** — in-process loop vs queue substrate; promote only if outer-loop behavior needs it. -- **Reconciliation chat lifecycle** — one persistent reconciliation chat per spec vs batch/invocation chats. -- **Generic sub-agent runs** — whether Track 2 owns a general sub-agent affordance or only the named secondary chat kinds for now. -- **Client patch-state migration** — stepwise renaming/folding into durable changesets. - -## 9. Traceability - -**Anchors** - -- Requirements 10, 39, 42, 44, 45. -- Assumptions A49, A88, A94, A95, A96. -- Decisions D135, D137, D138, D139, D140, D143, D146, D148, D149, D153, D154. -- Invariants I111, I112, I113, I114, I116, I117, I118, I120. -- PLAN frontier items: `chat-runtime-secondary-chats`, `reconciliation-runtime`, `changeset-ledger`, `chat-context-provision`. - -**Likely future decisions when Tracks 2–5 are scoped** - -- Secondary-chat lifecycle and anchor policy. -- Reconciliation chat lifecycle. -- Changeset migration sequence. -- Direct-edit attention/focus policy. -- Context-handle persistence/expiry policy after changeset-backed item versions exist. -- Snapshot-builder API and artifact schema boundaries for item, neighborhood, economic graph, and historical neighborhoods. - -**Cross-references** - -- [MULTI_CHAT.md](./MULTI_CHAT.md) §3 substrate, §4 context model, §5 reconciliation primitive -- [SIDE_CHAT.md](./SIDE_CHAT.md) §5 edit-patch routing, §13 substrate alignment -- [PATCH_LEDGER.md](./PATCH_LEDGER.md) §Reconciliation Flow, §Target Ordering, §Phase 2 Patch Ledger -- [CONTINUOUS_WORKSPACE_HYBRID.md](./CONTINUOUS_WORKSPACE_HYBRID.md) §Recommended direction -- [memory/SPEC.md](../../memory/SPEC.md) Future Direction Register and Lexicon -- [memory/PLAN.md](../../memory/PLAN.md) Sequencing, Dependencies, and runtime frontier definitions -- [SUBSTRATE_STRANGLER_COORDINATION.md](./SUBSTRATE_STRANGLER_COORDINATION.md) §Upcoming substrate waves and expected interfaces -- [INTENT_GRAPH_SEMANTICS.md](./INTENT_GRAPH_SEMANTICS.md) §Relation-policy registry and endpoint-relative labels diff --git a/archive/docs/design/DEFERRED_RECONCILIATIONS.md b/archive/docs/design/DEFERRED_RECONCILIATIONS.md deleted file mode 100644 index 52878265b..000000000 --- a/archive/docs/design/DEFERRED_RECONCILIATIONS.md +++ /dev/null @@ -1,63 +0,0 @@ -# Deferred Reconciliations — Audit Verdicts - -> Status: **audited 2026-05-13**. -> Original date: 2026-05-07. -> Scope: product-direction items derived from the archived intent-spec synthesis ([`INTENT_SPEC_EVOLUTION.md`](../archive/design/INTENT_SPEC_EVOLUTION.md)) that needed a decision: promote into `memory/SPEC.md` / `memory/PLAN.md`, keep gated, or retire as duplicate/deprecated. - -## Audit summary - -No item should be promoted into `memory/SPEC.md` or `memory/PLAN.md` immediately. - -| Theme | Verdict | Reason | -| --- | --- | --- | -| Spec drift surfacing | **Keep deferred** | The concept is still worth preserving, but it is not yet actionable as a requirement or horizon item until FE-700 lands typed checkability / witness metadata and FE-702-style probes can show drift detection is real rather than aspirational. `memory/SPEC.md` already has a lexicon entry for `spec drift`, which is enough for now. | -| Topology-driven disambiguation / next-question ranking | **Covered; do not promote standalone** | The useful part is already represented by the FE-700 semantic model and FE-702 behavioral-kernel / graph-review probe direction. It may later emerge as interviewer behavior, but promoting a separate horizon item now would duplicate those frontier items and over-specify mechanism before probes. | -| Edge epistemic metadata / relation participation | **Duplicate of current FE-700 direction** | `memory/SPEC.md` already records relation policy, support/status gating, operational directionality, edge-local neighborhoods, and relation-family vocabulary through Requirements 30/38, assumptions A81/A93, decisions D137/D150, invariants I109/I118, and lexicon rows. `memory/PLAN.md` FE-700 explicitly calls out edge epistemic metadata and relation-policy directionality. | - -## Retained deferred item - -### Spec drift surfacing - -**Deferred requirement candidate.** When a generated artifact (criterion, requirement, candidate-spec direction, export bundle, or downstream implementation behavior) diverges from its source intent, Brunch should surface the divergence in human terms — "original intent vs generated behavior vs potential mismatch" — so the user can validate meaning at the point where it could have changed, rather than after the divergence has been laundered into a final document. - -- **Current canonical coverage:** `memory/SPEC.md` lexicon entry `spec drift`; broader progressive-checkability direction in Requirement 38, A77/A78, D134, and FE-700. -- **Why not promote now:** drift detection needs typed checkability / witness metadata and generated-artifact comparison evidence. Without that substrate, a requirement or plan item would be vague product aspiration rather than an actionable frontier. -- **Trigger to revisit:** FE-700 lands typed checkability / witness metadata and FE-702 or a follow-on probe demonstrates at least one credible drift-detection workflow. -- **Likely promotion path after trigger:** run `ln-spec` to add a requirement and paired assumption; run `ln-plan` only if the probe supports a distinct product surface beyond FE-700/FE-702 follow-through. -- **Possible future design doc:** `docs/design/SPEC_DRIFT.md`, created only if the requirement is promoted. - -## Retired as standalone promotions - -### Topology-driven disambiguation / next-question ranking - -**Original impulse.** The interviewer could issue contrastive A/B/C disambiguation questions when graph topology reveals high-fanout assumptions, unwitnessed requirements, unverified invariants, decisions without rejected alternatives, goals without derived requirements, or conflicting constraints. - -**Audit verdict:** do not promote as a separate SPEC requirement or PLAN horizon item now. - -- **Captured by:** FE-700 intent graph semantics + relation-policy directionality; FE-702 graph-review / scenario-options probes; `docs/design/INTENT_GRAPH_SEMANTICS.md`; `docs/design/BEHAVIORAL_KERNELS.md`; A80/A85/A91; D134/D137/D151/D152. -- **Reason:** topology is one ranking signal inside the graph-review / behavioral-kernel direction, not a separate product capability yet. It should remain a probe hypothesis until the semantic substrate exists and kernel probes show that topology-driven ranking beats simpler prompt/context heuristics. -- **Future revisit condition:** if FE-702 probes demonstrate a specific topology-ranking algorithm that should become user-visible interviewer behavior, promote it through `ln-spec` / `ln-plan` then. - -### Edge epistemic metadata / relation participation rules - -**Original impulse.** Knowledge edges would carry support/status/provenance/rationale, and only certain support/status combinations would participate in cascade, staleness, export trace, reconciliation, and weak-suggestion behavior. - -**Audit verdict:** already adopted into canonical direction; no standalone promotion remains. - -- **Captured by:** Requirement 30, Requirement 38, A81, A93, D137, D150, I109, I118, and the lexicon rows for `edge-local neighborhood`, `relation family`, and `relation policy`; `memory/PLAN.md` FE-700 explicitly includes edge epistemic metadata and relation-policy directionality. -- **Reason:** keeping this as a pending promotion would create duplicate planning state. FE-700 is the right owning frontier. -- **Future revisit condition:** FE-700 implementation may refine field names or policy axes, but that should happen inside FE-700 scope rather than via this deferred ledger. - -## How to use this doc - -1. Keep this file only while **spec drift surfacing** remains a deferred, not-yet-actionable product impulse. -2. Before opening post-FE-700 semantic/generative work, check whether the spec-drift trigger has fired. -3. If the trigger fires, promote through the canonical skills: `ln-spec` for SPEC.md changes and `ln-plan` for PLAN.md changes. -4. If spec drift is promoted or explicitly retired, delete this file. The synthesis source remains in [`INTENT_SPEC_EVOLUTION.md`](../archive/design/INTENT_SPEC_EVOLUTION.md). - -## References - -- [`INTENT_SPEC_EVOLUTION.md`](../archive/design/INTENT_SPEC_EVOLUTION.md) — synthesis source for the original deferred impulses. -- [`INTENT_GRAPH_SEMANTICS.md`](./INTENT_GRAPH_SEMANTICS.md) — typed-graph reference and FE-700 semantic direction. -- [`BEHAVIORAL_KERNELS.md`](./BEHAVIORAL_KERNELS.md) — kernel-driven question reference, including topology-adjacent probe ideas. -- `memory/PLAN.md` Next items for FE-700 and FE-702 — the owning frontier items for the retired standalone topology and edge-metadata impulses. diff --git a/archive/docs/design/GRAPH_EDGE_CATEGORIES.md b/archive/docs/design/GRAPH_EDGE_CATEGORIES.md new file mode 100644 index 000000000..ec211a9a4 --- /dev/null +++ b/archive/docs/design/GRAPH_EDGE_CATEGORIES.md @@ -0,0 +1,441 @@ +# Graph Edge Categories — archived brainstorm + +> **Retired 2026-05-31.** Superseded by +> [`docs/design/GRAPH_MODEL.md`](../../../docs/design/GRAPH_MODEL.md), +> which is the canonical reference for the edge model. This document +> is preserved as the rationale lineage / stress-test record that +> produced GRAPH_MODEL.md. + +Long-form design note for the graph data layer. This document records the replacement direction for the earlier "large semantic edge-type catalogue + relation-policy interpretation" model discussed for M4/M5 graph work. + +`memory/SPEC.md` remains the authoritative register until this design is promoted through `ln-spec` / `ln-sync`. This document is the rationale, stress-test record, and prompt/tooling material for the agent-facing graph edge tools. + +## Starting point + +The current graph plan already requires: + +- durable graph nodes and edges in SQLite-backed graph persistence +- all mutations through `CommandExecutor` +- structural legality at write time +- graph snapshots / neighborhoods for agent context +- review-set proposals carrying entity and edge drafts that can dry-run through `CommandExecutor` +- edge semantics rich enough to support intent, oracle, design, and plan planes over time + +The original direction implied a catalogue of named semantic relation kinds, with tuple-specific legality and projection policy. Examples in current architecture notes include relation names like `depends_on`, `validates`, `produces`, `discharges`, `counterexample_for`, and `witnesses`. + +That shape creates two coupled burdens: + +1. **Modeling burden at edge creation.** The agent must choose the exact relation kind from a growing catalogue, and must not hallucinate relation kinds that do not exist. +2. **Interpretation burden at snapshot time.** Context builders must consult relation policy to determine whether a given typed relation makes the anchor's neighbor a dependency, dependent, support item, realization, boundary, related concern, and so on. + +This is manageable for a human-curated ontology. It is brittle when the primary edge author is an agent extracting or projecting graph mutations from assistant-user exchanges, review sets, and later reviewer outputs. + +## Design move + +Store a small set of **structural edge categories with endpoint roles**. Derive domain-specific labels later from tuple context. + +```text +agent-facing command + -> category + endpoint roles + -> CommandExecutor structural validation + -> stored graph edge + -> tuple-label lookup for snapshot rendering +``` + +The agent does **not** author labels like `depends_on`, `asserted_by`, `witnesses`, or `implements`. Those are presentation phrases derived from: + +```text +(category, source node kind, target node kind, endpoint perspective, stance?) +``` + +The category and endpoint roles drive policy. Tuple-specific label lookup must not upgrade, downgrade, or reinterpret the stored category. + +## Final category scheme + +```text +legend: + dependency: dependency -> dependent # hard upstream + support: support -> claim # soft warrant / evidence + realization: abstract -> concrete # expression / implementation / establishment + boundary: boundary -> subject # scope / constraint / exclusion + composition: whole -> part # containment / decomposition + association: peer <-> peer # weak relatedness only + supersession: successor -> predecessor # replacement / retirement lineage +``` + +### Category table + +| Category | Endpoint roles | Policy | Agent test | +| --- | --- | --- | --- | +| `dependency` | `dependency -> dependent` | Hard upstream. If the dependency changes or is invalidated, the dependent must be revisited, blocked, or marked stale. | “If this upstream item stops being true, must the downstream item be reconsidered?” | +| `support` | `support -> claim` | Soft warrant, motivation, evidence, example, or counterexample. Does not automatically block the claim. Carries `stance: for | against`. | “Does this item strengthen, motivate, witness, or challenge the claim without being load-bearing?” | +| `realization` | `abstract -> concrete` | A concrete item expresses, implements, operationalizes, establishes, or asserts an abstract item. | “Is the target a concrete form of the source?” | +| `boundary` | `boundary -> subject` | A constraint, non-goal, scope rule, or exclusion limits the subject. Boundary changes can require subject review, but boundary is not merely evidence. | “Does this item limit, scope, exclude, or constrain the subject?” | +| `composition` | `whole -> part` | Topology / decomposition. A whole contains a part. This is not a sequencing dependency. | “Is this a parent/child, whole/part, milestone/slice, or decomposition relation?” | +| `association` | `peer <-> peer` | Weak related concern. No dependency, support, cascade, or completion semantics. Last resort. | “Are these usefully related, but no stronger category is safe?” | +| `supersession` | `successor -> predecessor` | A successor replaces a predecessor for overlapping scope. Projections can hide superseded predecessors from active context while preserving history. Must be acyclic. | “Does this newer item intentionally replace the older item?” | + +## Edge shape + +Approximate persisted shape: + +```ts +type GraphEdgeCategory = + | "dependency" + | "support" + | "realization" + | "boundary" + | "composition" + | "association" + | "supersession"; + +type GraphEdge = { + id: string; + category: GraphEdgeCategory; + sourceId: string; + targetId: string; + + // Only valid for support. + stance?: "for" | "against"; + + // Grounding for why this accepted edge exists. + basis: "explicit" | "inferred" | "accepted_review_set"; + rationale?: string; + provenance?: { + sessionId?: string; + entryId?: string; + proposalEntryId?: string; + }; + + createdAtLsn: number; + updatedAtLsn: number; +}; +``` + +Graph truth should not carry low-confidence edge candidates as accepted edges. Low-confidence or uncertain material belongs in structured-exchange preface, `capture_*` analysis, review-set drafts, or `reconciliation_need` until clarified or accepted. + +## Agent-facing tool surface + +Prefer category-specific commands over one generic `createEdge({ relationKind })` command: + +```ts +linkDependency({ dependency, dependent, basis, rationale }) +linkSupport({ support, claim, stance, basis, rationale }) +linkRealization({ abstract, concrete, basis, rationale }) +linkBoundary({ boundary, subject, basis, rationale }) +linkComposition({ whole, part, basis, rationale }) +linkAssociation({ a, b, basis, rationale }) +linkSupersession({ successor, predecessor, basis, rationale }) +``` + +The command layer owns tuple validation. If a tuple is structurally illegal for a category, the tool returns `structural_illegal`; the agent should not try to invent a narrower label to force it through. + +## Label lookup + +Tuple-label lookup is a presentation concern only. It produces plain/pseudo language for graph snapshots, UI, and prompt context. + +Examples: + +| Stored edge | View from source | View from target | +| --- | --- | --- | +| `dependency(assumption -> decision)` | “premise for decision” | “depends on assumption” | +| `dependency(assumption -> requirement)` | “required by requirement” | “depends on assumption” | +| `support(context -> requirement, for)` | “motivates requirement” | “motivated by context” | +| `support(example -> invariant, for)` | “witnesses invariant” | “witnessed by example” | +| `support(example -> invariant, against)` | “counterexample for invariant” | “challenged by counterexample” | +| `realization(invariant -> requirement)` | “expressed by requirement” | “expresses invariant” | +| `realization(requirement -> design module)` | “realized by module” | “realizes requirement” | +| `realization(interface -> adapter)` | “implemented by adapter” | “implements interface” | +| `realization(requirement -> plan slice)` | “established by slice” | “establishes requirement” | +| `boundary(non-goal -> requirement)` | “rules out / limits requirement” | “bounded by non-goal” | +| `composition(milestone -> slice)` | “contains slice” | “belongs to milestone” | +| `supersession(new requirement -> old requirement)` | “supersedes old requirement” | “superseded by new requirement” | +| `association(A <-> B)` | “related to B” | “related to A” | + +Snapshot buckets come from category and endpoint role, not from the derived label. + +```text +anchor: R_offline: intent.requirement + +hard dependencies: + A_no_network: depends on assumption + +support: + P_field_users: motivated by context + E_airplane: witnessed by example + +realized by: + M_sqlite_store: realized by design module + SL_persist: established by plan slice + +boundaries: + C_no_cloud: bounded by constraint + +supersedes: + R_offline_v0: supersedes prior requirement +``` + +## Prompting snippets for graph-writing agents + +### System prompt fragment + +```text +When creating graph edges, choose only from Brunch's structural edge categories: +dependency, support, realization, boundary, composition, association, supersession. + +Do not invent relation names such as depends_on, validates, witnesses, implements, +expresses, motivated_by, or related_concern. Those are rendering labels derived later +from the stored category and endpoint node kinds. + +Create an accepted graph edge only when the relation is clear enough to become graph truth. +If the relation is weak, speculative, ambiguous, or merely a possible duplicate/possible +relation, do not create an accepted edge. Keep it in preface/capture analysis or raise a +reconciliation_need. + +Use one edge for the strongest operational role between two nodes. Do not create multiple +edges merely because several English paraphrases are possible. +``` + +### Category selection rubric + +Ask these questions in order. Stop at the first strong match. + +```text +0. Should this be graph truth now? + - explicit user statement, accepted review set, or high-confidence extraction -> continue + - weak inference, possible relation, possible duplicate, unresolved ambiguity -> no accepted edge + +1. Is a newer item intentionally replacing an older item for overlapping scope? + -> supersession(successor -> predecessor) + +2. Is this a whole/part, parent/child, or decomposition relation? + -> composition(whole -> part) + +3. Does one item limit, exclude, scope, or constrain another? + -> boundary(boundary -> subject) + +4. If the upstream item is invalidated, must the downstream item be revisited, blocked, or marked stale? + -> dependency(dependency -> dependent) + +5. Is one item a concrete expression, implementation, assertion, establishment, or operationalization of another? + -> realization(abstract -> concrete) + +6. Does one item motivate, justify, evidence, witness, or challenge another without being load-bearing? + -> support(support -> claim, stance: for | against) + +7. Are the two items usefully related, but no stronger role is safe? + -> association(a <-> b) + +8. Otherwise, create no edge. +``` + +### Hard vs soft upstream guard + +```text +Do not treat every “because” as dependency. + +Use dependency only when downstream validity or readiness depends on the upstream item. +Use support when the upstream item explains, motivates, or evidences the downstream item +but the downstream item could still stand if the support changed. +``` + +Examples: + +```text +“A local-only product means we do not need account auth.” + local-only assumption -> no-auth decision + category: dependency + +“Terminal users will appreciate keyboard-first flow.” + terminal-user context -> keyboard-first requirement + category: support, stance: for +``` + +### Evidence guard + +```text +Evidence is support unless and until it needs a distinct policy. + +Positive example -> claim: support(..., stance: for) +Counterexample -> claim: support(..., stance: against) +Check result -> criterion/invariant: support(..., stance: for | against) +``` + +Do not create a separate `evidence` edge category unless support proves insufficient for policy. + +### Boundary vs negative support guard + +```text +Use boundary when the source is itself a scope rule, constraint, non-goal, exclusion, or limit. +Use support(..., stance: against) when the source is evidence or an example that argues against a claim. +``` + +Examples: + +```text +“No cloud accounts” -> “sync via hosted account” + category: boundary + +“Cloud outage blocks access” -> “hosted account sync is acceptable” + category: support, stance: against +``` + +### Composition vs dependency guard + +```text +Containment is not dependency. + +A milestone contains a slice: composition. +A slice cannot start until another slice lands: dependency. +A module contains a private helper: composition. +A module calls another module at runtime or requires its interface: dependency or realization, depending on the claim. +``` + +### Realization guard + +```text +Use realization for abstract-to-concrete expression: +- invariant -> requirement +- requirement -> design module +- interface -> adapter +- requirement -> plan slice +- oracle strategy -> concrete check + +If the relation only explains why the concrete item exists, use support. +If the abstract item must remain true for the concrete item to remain valid, use dependency. +``` + +### Association guard + +```text +Association is not a junk drawer. +Use it only when preserving adjacency is useful for future context and no stronger category is justified. +Do not create association for ordinary co-mention in the same sentence or turn. +Prefer no edge over noisy association. +``` + +### Supersession guard + +```text +Use supersession only for intentional replacement of overlapping scope. +It is not support, dependency, or composition. +It should be acyclic. Context projections should prefer active leaves of a supersession chain +while preserving the predecessor in history/audit views. +``` + +Caveat: Brunch already uses `supersedes` in transcript-native review-set proposal regeneration. Graph `supersession` is a graph-entity relation; proposal `supersedes` is transcript lineage. They share a concept but are separate substrates. + +## Worked stress cases + +### Case 1 — one category set across intent, design, oracle, and plan + +```text +nodes: + A_local_only: intent.assumption + D_no_auth: intent.decision + R_offline: intent.requirement + I_no_network: intent.invariant + C_no_cloud: intent.constraint + + IF_session_store: design.interface + M_sqlite_store: design.module + + CH_airplane: oracle.check + EV_trace: oracle.evidence + + MS_graph: plan.milestone + SL_persist: plan.slice + +edges: + A_local_only -[dependency]-> D_no_auth + A_local_only -[dependency]-> R_offline + C_no_cloud -[boundary]-> D_no_auth + + I_no_network -[realization]-> R_offline + R_offline -[realization]-> M_sqlite_store + IF_session_store -[realization]-> M_sqlite_store + + CH_airplane -[support:+]-> I_no_network + EV_trace -[support:+]-> CH_airplane + + MS_graph -[composition]-> SL_persist + R_offline -[realization]-> SL_persist +``` + +Pressure: `realization` is deliberately broad. It covers expression, implementation, assertion, and establishment. The bet is that category policy remains stable while tuple labels make each relation readable. + +### Case 2 — support vs realization for examples + +```text +nodes: + I_preserve_history: intent.invariant + R_archive_tasks: intent.requirement + E_delete_project: intent.example + CR_history_visible: intent.criterion + +edges: + I_preserve_history -[realization]-> R_archive_tasks + I_preserve_history -[realization]-> CR_history_visible + E_delete_project -[support:+]-> I_preserve_history + E_delete_project -[support:+]-> CR_history_visible +``` + +Rule: examples usually support claims; they do not usually realize them. Use `realization` for durable work-products that operationalize an abstract claim. + +### Case 3 — process debt instead of accepted graph truth + +```text +nodes: + R_fast_setup: intent.requirement + R_quick_onboard: intent.requirement + N_possible_dup: reconciliation_need + +? possible edge: + R_fast_setup -[association]-> R_quick_onboard + +non_semantic_refs: + N_possible_dup -[concerns]-> R_fast_setup + N_possible_dup -[concerns]-> R_quick_onboard +``` + +Rule: suspected duplicate / possible relation is process debt, not graph truth. Raise `reconciliation_need` unless weak association is actually useful as accepted context. + +### Case 4 — supersession is the first non-fitting relation, now admitted explicitly + +```text +nodes: + R_old: intent.requirement [superseded] + R_new: intent.requirement [active] + D_keychain: intent.decision + +edges: + D_keychain -[realization]-> R_new + R_new -[supersession]-> R_old +``` + +Reasoning: replacement does not fit dependency, support, realization, boundary, composition, or association cleanly. It is temporal/evolutionary and affects active-context projection. Admit it as a first-class category with tight caveats rather than smuggling it into association. + +## Structural invariants + +- Edge categories are closed. Agents cannot submit arbitrary relation strings. +- Every edge has exactly one category. +- `support.stance` is required for `support` and invalid for other categories. +- `association` is symmetric at the product level, even if physically stored with source/target ids. +- `supersession` chains are acyclic. +- Accepted graph edges are graph truth. Candidate or low-confidence edges live outside graph truth until accepted. +- Tuple-label lookup cannot change category policy. +- Snapshot bucket assignment comes from category and endpoint role, not from label strings. +- `composition` does not imply sequencing or dependency. +- `support` does not imply blocking/staleness by default. + +## Open assumptions + +- The seven-category set is expressive enough for M4/M5 intent-first work and later oracle/design/plan stubs. +- `realization` can remain broad if tuple labels carry the domain-specific phrasing. +- `support` can absorb evidence, examples, checks, and counterexamples with only `stance` as extra structure. +- `association` will remain rare under prompting discipline. +- `supersession` belongs in graph edges rather than only lifecycle/change-log fields because active-context projection needs to traverse replacement lineage. + +## Promotion notes + +When this is promoted into canonical planning/spec state, update the graph-data-plane language from “edge-type catalogue” to “closed structural edge category set,” and update review-set proposal payload examples so `edge_drafts` use category-specific endpoint roles instead of arbitrary `relation` strings. diff --git a/archive/docs/design/GRAPH_KIND_CHIP_TOGGLE.md b/archive/docs/design/GRAPH_KIND_CHIP_TOGGLE.md deleted file mode 100644 index cd112665e..000000000 --- a/archive/docs/design/GRAPH_KIND_CHIP_TOGGLE.md +++ /dev/null @@ -1,143 +0,0 @@ -# Graph-View Kind Chip — Split-Button Toggle - -> Output of brainstorm session 2026-05-01. Refines the kind-filter row at the top of the graph view (`KindFilterToggler`) into a split-button chip with separate navigate and visibility actions. -> -> Status: **proposed** — pending review before transitioning to implementation plan. - -## 1. Concept & problem - -Today, each chip in the graph view's kind-filter row is a single button: clicking it toggles whether items of that kind are rendered in the structured list. The chip has no second function, so users who want to *jump to* a kind's section must scroll manually past kinds they're not interested in. The structured-list section headers themselves are not anchor targets. - -The chip should do both things: act as a quick navigation control (scroll to that kind's section) and as a visibility filter (show/hide that kind from the page). The two actions need to be discoverable and independent — you should be able to navigate without affecting visibility, and toggle without scrolling. - -## 2. Anatomy - -Each chip is a single visual unit divided into two click zones by a thin internal border. - -``` -┌──────────────────────────┬────┐ -│ [G] Goals · 12 │ 👁 │ -└──────────────────────────┴────┘ - body → scroll-to-section toggle → hide/show -``` - -- **Body** (kind swatch + label + count): primary action is *navigate*. Click → smooth-scroll to the kind's section header in the structured list. -- **Toggle** (eye-open / eye-slash icon): primary action is *show/hide*. Click → flip `hiddenKinds` membership for that kind. No scroll. - -The two zones are independent click targets and independent tab stops. Hovering one does not highlight the other. - -## 3. Visual states - -| State | Border | Swatch | Label / count | Toggle icon | -| ---------------- | --------- | ---------- | ------------- | ----------- | -| Visible | solid 1px | full color | full color | eye-open | -| Hidden | dashed 1px| greyed | greyed | eye-slash | -| Hover (per zone) | unchanged | unchanged | unchanged | subtle bg fill on hovered zone only | -| Focus | unchanged | unchanged | unchanged | focus ring on focused zone | - -The dashed-border + greyed combination signals "this kind is off-screen." The eye-slash icon reinforces the same meaning at the toggle. Hover affordance is local to the zone the cursor is over so the user can see which click target they're about to hit. - -## 4. Behavior - -### 4.1 Body click - -| Kind visible | Kind hidden | -| --- | --- | -| Smooth-scroll to the section containing that kind. No state change. | `flushSync` the kind out of `hiddenKinds` first, then `navigate({ hash })`. The unhide commits synchronously before the hash update triggers `useGraphHashAnchor`'s effect, so the target row/anchor is mounted by the time scroll runs. | - -The `flushSync` wrapper is necessary because `useGraphHashAnchor` (lines 23–49 of `-structured-list-view.tsx` today) keys its effect on `targetRef` only — if the hash changes and the target isn't yet mounted, the anchor effect runs once, finds nothing, and won't retry when the row later appears. Wrapping the unhide in `flushSync` guarantees the row is in the DOM before navigate fires. - -A parallel branch (FE-655, `ka/fe-655-re-land-orphaned-graph-prs`, commit `c37cdb4`) introduced the same pattern for `RelationChip` auto-unhide. If FE-655 lands first, the helper extracted there can be reused; otherwise this work introduces it. Either way, by the end of this work both the new top-chip body and the relation-chip click should call the same `unhideAndNavigate` helper. - -### 4.2 Toggle click - -Pure state mutation: flip `hiddenKinds` membership for that kind. No scroll, no hash change. - -### 4.3 Bulk control — "Show all" - -A trailing **Show all** text-link appears at the right end of the chip row only when `hiddenKinds.size > 0`. Clicking resets `hiddenKinds` to the empty set. When everything is visible the link is not rendered (zero footprint). - -Solo / hide-others gestures (e.g. alt-click) are explicitly out of scope — undiscoverable, deferrable. - -## 5. State & persistence - -`hiddenKinds` stays as local React state on the structured-list component, as it is today. No URL search-param, no `localStorage`. Refresh resets the row to all-visible. - -Rationale: the filter is a temporary visual reduction during a single graph-view session, not a saved preference. Persisting it would surprise users who hide a kind, navigate away, and return (or refresh) without remembering what they turned off. Upgrading to URL-state later, if shareable links become useful, is a small change to one component. - -## 6. Accessibility - -- **Body button**: ` - - - ); -} -``` - -The swatch reuses the existing `kindColor` / `kindTextColor` maps from `@/client/components/knowledge-card`, matching how the row badges and the current chip render today. `Eye`/`EyeOff` come from `lucide-react` (already used elsewhere in this file). The `data-graph-kind-body` and `data-graph-kind-toggle` attributes keep test selectors compositional and let `useGraphHashAnchor` discover the anchor target if needed. - -## 8. Out of scope - -- Solo / alt-click hide-others. -- Per-relation-type filtering (chips here are kind-level, not relation-level). -- Persisting filter state across refresh / across sessions. -- URL-state for hidden kinds. -- Animated transitions between visible/hidden states beyond the CSS color/border transition. -- Right-sidebar bundle filtering (separate concern from the kind-row chips). - -## 9. Verification approach - -- **F1 component tests**: render `KindToggleChip` in visible/hidden states; assert body click invokes `onNavigate(kind)` only, toggle click invokes `onToggle(kind)` only; assert `aria-pressed` flips with `isHidden`. -- **F2 router-integrated tests**: - - Body click on visible chip → URL hash updates → smooth-scroll triggered to matching anchor element. - - Body click on hidden chip → `hiddenKinds` drops the kind in the same render pass (`flushSync`), then scroll lands on the now-visible section. - - Toggle click → `hiddenKinds` flips, no hash change, no scroll. - - "Show all" appears iff `hiddenKinds.size > 0`; clicking resets the set. -- **A11y**: keyboard tab order matches §6; toggle's `aria-pressed` reflects state; both buttons have non-empty accessible names. - -## 10. Open questions - -- **Anchor granularity.** §7.1 proposes per-kind anchors inside display-group sections. Implementation could instead scroll to the display-group header (coarser — multiple chips would scroll to the same place) or to the first row of each kind (finer — current proposal). First-row-of-kind is more precise but requires marking the boundary; group-header is simpler but feels imprecise. Default: per-kind anchor. -- **FE-655 ordering.** The `unhideAndNavigate` helper described in §7.1 is also being added on `ka/fe-655-re-land-orphaned-graph-prs` for `RelationChip`. If FE-655 merges first, this work imports the helper; otherwise this work introduces it and FE-655 reuses it on rebase. Either order is fine — captured here so the author knows to coordinate, not duplicate. - -Persistence (§5) was confirmed as local-state-only. Bulk gesture beyond "Show all" was confirmed as deferred. diff --git a/archive/docs/design/INTENT_GRAPH_SEMANTICS.md b/archive/docs/design/INTENT_GRAPH_SEMANTICS.md deleted file mode 100644 index d59e26094..000000000 --- a/archive/docs/design/INTENT_GRAPH_SEMANTICS.md +++ /dev/null @@ -1,460 +0,0 @@ -# Intent Graph Semantics — Ontology, Edges, and Progressive Checkability - -> Status: **working design proposal**. -> Date: 2026-05-07. -> Scope: the **product-layer** intent-graph ontology and its edge semantics — the typed kinds, subtypes, relations, edge metadata, relation-policy registry, observer-prompt classification rules, and topology-driven question-ranking heuristics that the Brunch product should converge on. -> -> This document is the canonical reference for the FE-700 frontier item ("Intent graph semantics + progressive checkability foundation") in `memory/PLAN.md`. It expands the `Recommended shape:` of that item with the full ontology and policy detail that is too long to live inside the plan. -> -> Source synthesis: [`INTENT_SPEC_EVOLUTION.md`](../archive/design/INTENT_SPEC_EVOLUTION.md) §3, §4, §6, §11. Where this document overlaps, it supersedes the synthesis as the structured reference; the synthesis remains the broader narrative. -> -> Layer note: this is the **product layer**. It describes what Brunch users build. The dev-layer ontology is a parallel-but-not-yet-converged register described in [`ln-skills/EVOLUTION.md`](./ln-skills/EVOLUTION.md). - -## Why this note exists - -The product ontology has been growing piecemeal. Today's exploration ontology (`goal`, `term`, `context`, `constraint`, `decision`, `assumption`) plus accepted-review materializations (`requirement`, `criterion`) is functional but imprecise: - -- A "decision" can absorb any user answer and become bloated. -- A "constraint" mixes scope boundaries, technical limitations, policy, and non-goals into one bucket. -- "Context" is doing the work of background, premises, descriptive truth, and pre-commitment notes simultaneously. -- An invariant (a property that must hold) has no home except as informal text inside a requirement or constraint. -- An example (a concrete case that disambiguates intent) is captured only as a turn artifact and lost as durable evidence. -- Edges (`depends_on`, `derived_from`, `constrains`, `verifies`, `refines`) carry no epistemic metadata — every edge is treated as equally authoritative for cascade, export, staleness, and reconciliation. - -The cumulative effect: the graph carries the right *vocabulary* but does not carry enough *typing* to drive cascade preview, witness generation, ambiguity discovery, or progressive-checkability projection without the LLM re-deriving structure on every call. - -This document specifies the typed shape FE-700 should land. - -## Top-level kinds - -Nine top-level kinds. The current six exploration kinds plus the two review-materialized kinds become eight, plus `example` is promoted to a top-level kind. Decision is narrowed; constraint is subtyped. - -| Kind | Modality of claim | Source | -| --- | --- | --- | -| `goal` | Value or outcome claim | "What outcome are we after?" | -| `context` | Descriptive claim | "What is true about the world this lives in?" | -| `constraint` | Boundary claim | "What does this rule out?" | -| `assumption` | Uncertainty claim | "What might be false?" | -| `decision` | Choice claim | "What did we pick among real alternatives?" | -| `requirement` | Obligation claim | "What must the system do?" | -| `invariant` | Preservation claim | "What must never be broken?" | -| `criterion` | Oracle claim | "How will we judge that it holds?" | -| `example` | Witness or disambiguator claim | "What concrete case would settle this?" | - -The framing: **a spec is a graph of typed claims.** Each kind is a *modality* of claim, not just a section bucket. `term` remains a vocabulary / lexicon capture used during grounding; it is not part of this typed-claim kind set until a future lexicon model promotes terms into graph-addressable claim records. - -### Notes on each kind - -**`goal`** — value or outcome claim. Why a feature exists. Does not commit the system to any particular behavior; commits the team to a target. - -**`context`** — descriptive claim about the world that would remain true even if the specification paused tomorrow. Background, premises, actors, repo facts, vocabulary. Carries promotion rules (below). - -**`constraint`** — boundary on acceptable solutions. Subtyped (below). Includes non-goals as a subtype. Distinct from invariant: a constraint restricts the *solution space*; an invariant states what must remain true as the system *operates or evolves*. - -**`assumption`** — material belief whose truth could be falsified later. Carries confidence and a validation approach. - -**`decision`** — chosen direction among plausible alternatives. **Narrow definition.** A decision is not every user answer; it is a choice with durable consequences. (See "Decision-capture criteria" below.) - -**`requirement`** — normative obligation: the system shall satisfy these properties. Materialized only through accepted requirements review today; once shared-property modeling lands, may also be created as a commitment to one or more `Property` records. - -**`invariant`** — property that must remain true across relevant states, transitions, executions, versions, or semantic revisions. Subtyped (below). - -**`criterion`** — observation that would witness a property holding. Subtyped (below). Distinct from a requirement: a requirement is what must be true; a criterion is how we recognize that it is. - -**`example`** — concrete scenario, trace, input/output, edge case, approved example, rejected example, not-relevant label, or counterexample. Subtyped (below). Durable evidence, not just conversational aid. - -## Subtypes - -Subtypes live inside a kind. They keep the top-level kind set small while preserving the discriminations the LLM needs to classify, observe, and check. - -### `constraint` subtypes - -| Subtype | Meaning | -| --- | --- | -| `non_goal` | An explicit exclusion from the current scope. | -| `scope` | A bound on what the spec covers vs. what it does not. | -| `technical` | A technical limitation imposed by stack, runtime, or platform. | -| `policy` | A policy or compliance restriction. | -| `resource` | A resource bound (cost, time, headcount, capacity). | -| `compatibility` | A compatibility constraint with existing systems, data, or interfaces. | -| `environmental` | An environmental constraint (deployment target, network shape, single-tenant). | - -### `criterion` subtypes - -| Subtype | Meaning | -| --- | --- | -| `acceptance` | A user-facing accept/reject condition. | -| `test` | An automated test. | -| `manual_review` | A reviewer-evaluated check. | -| `runtime_check` | A runtime assertion or contract. | -| `proof` | A proof obligation in a formal system. | -| `observability` | A trace, log, or audit signal that must be visible. | - -Required fields on a criterion: `target` (which requirement / invariant / property it observes), `method`, `scope` (`example` / `bounded` / `all_states`), `expected_observation`. - -### `invariant` subtypes - -| Subtype | Meaning | -| --- | --- | -| `state` | A property that holds in every reachable state. | -| `transition` | A property of every state transition. | -| `authority` | A property about who or what may take an action. | -| `provenance` | A property about where data or decisions originated. | -| `consistency` | A consistency property between two views, projections, or copies. | -| `security` | A security or access-control property. | -| `data_integrity` | An integrity property over stored or transmitted data. | - -### `example` subtypes - -| Subtype | Meaning | -| --- | --- | -| `positive` | A concrete case that the spec must accept. | -| `negative` | A counterexample: a concrete case that the spec must reject. | -| `edge_case` | A boundary or degenerate case included for clarification. | -| `trace` | A sequence of states or actions that illustrates a behavior. | -| `not_relevant` | A case the user labelled out of scope, useful as durable disambiguation. | - -The `negative` subtype is especially important because **intent is often clarified by ruling out plausible interpretations** — see Negative edges below. - -## Promotion rules - -The interviewer and observer should treat the kinds as a partial lattice with explicit promotion rules. The most common drift case is `context` absorbing material that should be a stronger kind. - -### `context` promotion - -| If the context… | Promote it to… | -| --- | --- | -| must be true for success | `requirement` or `invariant` | -| limits acceptable solutions | `constraint` | -| may be false and matters | `assumption` | -| chooses among alternatives | `decision` | -| just helps interpretation | keep as `context` | - -### `requirement` ↔ `invariant` - -A requirement says "the system must do X." An invariant says "X must never be broken." They often pair: a requirement to *do* something, plus an invariant to *preserve* something across the doing of it. - -### `decision` ↔ `invariant` - -A decision captures the choice; an invariant captures the rule that must keep holding after the choice. "We chose option A over option B" is a decision. "After this choice, property P must continue to hold" is the invariant the decision introduces. - -### `assumption` retirement - -When an assumption is validated, it does not become a requirement. It becomes either a **decision** (if the validation forced a choice) or it gets retired as confirmed truth (and the dependent decisions / requirements no longer carry the assumption tag). - -## Decision-capture criteria - -A common drift case is treating every user answer as a decision. A claim should become a `decision` only if it satisfies all of the following: - -1. **Plausible alternatives existed.** "We chose React over Svelte" is a decision; "we use TypeScript" is context if no alternative was on the table. -2. **The choice is durable.** It will affect future design, implementation, or interpretation. One-off question answers that don't constrain future work are not decisions. -3. **The choice is explicit.** It can be stated as "we chose A over B/C/D" rather than as a description of current behavior. -4. **Rejected alternatives can be named.** A decision without rejected alternatives is just a description. -5. **There is a rationale.** "Because X" or "because Y was a non-starter for Z reason." A decision without rationale is just a fact. - -Required fields on a decision: `chosen_option`, `rejected_alternatives` (≥ 1), `rationale`, `scope` (where this decision applies), `consequences` (what it now constrains downstream). - -## Observer-prompt classification guide - -When the observer extracts knowledge items from an answered turn, it should use a one-line rule per kind to decide how to classify a span of conversational content: - -| Kind | One-line classification rule | -| --- | --- | -| `goal` | "X so that Y" or "we want Y" — outcome statement, no specific implementation | -| `context` | Descriptive present-tense fact about the world that does not commit the system | -| `constraint` | "must not", "cannot", "only if" — bounds the solution space | -| `assumption` | "we think", "probably", "if X is true" — material belief that could be wrong | -| `decision` | "we chose A over B because" — see Decision-capture criteria above | -| `requirement` | "the system shall" / "must do" — obligation, materialized via accepted review only | -| `invariant` | "always true", "never", "must remain" — preservation across states/transitions | -| `criterion` | "we'll know it works when", "tested by", "we'll review for" — oracle for a property | -| `example` | "for instance", "like when", "what about the case where" — concrete witness | - -The observer should **abstain** rather than guess when classification support is weak. Speculative captures degrade graph signal. - -## Phase-by-phase capture mapping - -The phase a turn belongs to is itself a strong classification prior. The observer's allowed captures per phase: - -| Phase | Allowed captures | Materialized at review acceptance | -| --- | --- | --- | -| Grounding | typed claims: `goal`, `context`, `constraint`, `assumption`, `example`; vocabulary capture: `term` | — | -| Design | `decision`, `constraint`, `invariant`, requirement-candidate (held as a draft tag), `example` | — | -| Requirements review | review proposes durable `requirement` items + paired `invariant` items | `requirement`, `invariant` materialize on accept | -| Criteria review | review proposes `criterion` items + `example` items + verification mappings | `criterion`, `example` materialize on accept | - -The conceptual shift from earlier exploration ontology is that **hardening is requirements + invariants + criteria + examples**, not just requirements + criteria. The intent-spec direction needs preservation claims and witness claims as durable, not as conversational. - -## Relations — the five-family taxonomy - -Relations are typed and grouped into five semantic families. Edge kinds say *how* claims justify, constrain, depend on, refine, and verify one another. - -| Family | Example relations | Purpose | -| --- | --- | --- | -| **Justification** | `derived_from`, `motivated_by`, `supports` | Explain why a claim exists | -| **Dependency** | `depends_on`, `assumes`, `requires` | Explain what must remain valid | -| **Boundary** | `constrains`, `excludes`, `rules_out`, `bounds_scope_of` | Explain how one claim limits another | -| **Refinement** | `refines`, `specializes`, `decomposes` | Explain how claims become more specific | -| **Verification** | `verifies`, `illustrates`, `disambiguates`, `counterexample_for`, `tested_by` | Connect intent to evidence | - -The current relation vocabulary in the schema (`depends_on`, `derived_from`, `constrains`, `verifies`, `refines`) maps cleanly onto four of the five families. The two new candidates worth highlighting: - -- **`illustrates`** and **`disambiguates`** (Verification family) — connect an `example` to the requirement, invariant, or decision it makes concrete. -- **`rules_out`** and **`counterexample_for`** (Boundary / Verification) — negative relations that connect a counterexample or constraint to the interpretations it eliminates. - -### Negative edges - -Intent is often clarified by ruling out plausible interpretations. Negative edges deserve first-class treatment: - -``` -Counterexample CE1: - "Rejected review item appears in export." - -CE1 violates Invariant I-review-authority. -Constraint C-no-fake-closure rules_out Requirement candidate "auto-export draft reviews". -``` - -Without negative edges, the graph captures only what we want; with them, the graph captures what we have *decided not to want*, which is often the harder-won knowledge. - -## Edge schema and epistemic metadata - -Every edge carries epistemic metadata so that inferred relations do not silently become false dependencies. - -```ts -type KnowledgeEdge = { - sourceId: KnowledgeItemId - targetId: KnowledgeItemId - relation: RelationKind - family: RelationFamily - support: 'explicit' | 'strong_inference' | 'weak_candidate' - status: 'proposed' | 'accepted' | 'rejected' | 'stale' - provenanceTurnId?: TurnId - rationale?: string - createdAt: timestamp - updatedAt: timestamp -} -``` - -| Field | Purpose | -| --- | --- | -| `support` | How well the edge is grounded. `explicit` = stated by the user; `strong_inference` = LLM-derived from a clear textual signal; `weak_candidate` = speculative pattern match. | -| `status` | Lifecycle. `proposed` = pending review; `accepted` = active; `rejected` = considered and dismissed; `stale` = upstream changed and needs reconfirmation. | -| `provenanceTurnId` | The turn this edge was extracted from, when known. | -| `rationale` | Short user-legible explanation, especially for inferred edges. | - -## Relation-policy registry - -Not every visible graph edge should drive cascade, staleness, export explanation, criteria generation, or the same compact-context wording. The relation-policy registry assigns capabilities per relation, gated by edge `support` and `status`, and owns both operational directionality and endpoint-relative display labels. Code must not infer downstream/upstream impact or dependency/dependent wording from raw source/target coordinates alone. - -| Axis | Meaning | -| --- | --- | -| `visible` | Render in graph view | -| `cascade` | Participate in cascade preview when source changes | -| `export_trace` | Appear in export rationale ("requirement R is here because of goal G") | -| `staleness` | Mark target as stale when source changes | -| `reconciliation` | Generate a `reconciliation_need` when source changes | -| `criteria_help` | Used by interviewer to suggest criteria for the target | -| `weak_suggestion` | LLM-only signal; never user-visible by default | -| `source_label` | Phrase used when rendering the edge from the source item's perspective. | -| `target_label` | Phrase used when rendering the same edge from the target item's perspective. | -| `source_change` / `target_change` | Which endpoint may require reconciliation when either endpoint changes. | - -A row in the registry might say: - -| Relation | Family | visible | cascade | export_trace | staleness | reconciliation | criteria_help | weak_suggestion | -| --- | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| `derived_from` | Justification | ✓ | ✓ | ✓ | ✓ | ✓ | — | — | -| `depends_on` | Dependency | ✓ | ✓ | — | ✓ | ✓ | — | — | -| `verifies` | Verification | ✓ | — | ✓ | ✓ | — | ✓ | — | -| `illustrates` | Verification | ✓ | — | ✓ | — | — | ✓ | — | -| `disambiguates` | Verification | ✓ | — | ✓ | — | — | ✓ | — | -| `rules_out` | Boundary | ✓ | ✓ | ✓ | — | ✓ | — | — | -| `related_to` *(catch-all)* | — | ✓ | — | — | — | — | — | ✓ | - -The actual registry should evolve through corpus probes. The point is that **policy is per-relation, per-axis**, not a binary "this edge counts." - -### Endpoint-relative labels for compact snapshots - -Compact context snapshots need both readable directions for an edge. A single canonical triple is not enough when the snapshot is centered on either endpoint. - -For an item-centered snapshot, relation policy should be able to render edges into buckets such as `dependencies` and `dependents` with phrases that make sense from the anchored item's perspective: - -```text -X123: Lorem ipsum... - dependents: - constrains X200: Lorem ipsum... - motivates X198: Lorem ipsum... - dependencies: - presumes X2: Lorem ipsum... - is conditioned by X78: Lorem ipsum... - proves X45: Lorem ipsum... -``` - -The bucket and phrase are relation-policy outputs, not string reversals. Some relations have natural canonical source-to-target wording (`constraint constrains requirement`), while others naturally point from a dependent claim to a premise (`decision assumes assumption`) or from an oracle to a claim (`criterion verifies requirement`). The registry must therefore store endpoint-relative labels and operational source-change/target-change behavior separately. - -Neighborhood snapshots can be selected by task rather than by one universal radius: - -- **Immediate adjacency** — all relation-policy-visible incident edges around the anchor; good default for side chat orientation. -- **Dependencies** — premises, constraints, assumptions, motivating goals, and evidence the anchor relies on; useful for “why does this item stand?” questions. -- **Dependents / impact** — claims likely to be affected if the anchor changes; useful for edit preview, cascade, and reconciliation. -- **Evidence** — examples, criteria, witnesses, counterexamples, and checkability context; useful for QA and verification discussion. -- **Reconciliation** — open needs, affected targets, source changes, and relation-policy rationale; useful for reconciliation chats. -- **Historical** — once changesets exist, the neighborhood around the changeset that originally captured an item or last updated it; useful for reviving the context in which a claim was made, not just its current graph surroundings. - -Historical neighborhoods should be changeset-derived, not approximated from current graph order. Before the changeset ledger, snapshot builders should avoid pretending they can answer “what was around this item when it was captured?” with precision. - -A relation policy row should support at least: - -```ts -type RelationPolicy = { - relation: RelationKind - canonicalLabel: string - sourceLabel: string - targetLabel: string - sourceRole: string - targetRole: string - sourceBucketForTargetSnapshot: 'dependencies' | 'dependents' | 'evidence' | 'contextual' - targetBucketForSourceSnapshot: 'dependencies' | 'dependents' | 'evidence' | 'contextual' - onSourceChange: 'affects_source' | 'affects_target' | 'affects_both' | 'none' | 'contextual' - onTargetChange: 'affects_source' | 'affects_target' | 'affects_both' | 'none' | 'contextual' -} -``` - -The exact bucket enum can be narrower in implementation, but the invariant is that context packs and reconciliation never recover directionality from verb names alone. - -## Edge-local neighborhoods - -For LLM collaboration the most important practical change is to provide **edge-local neighborhoods**, not only grouped item lists. A neighborhood pack for one claim: - -``` -R17: Each phase exposes an explicit kickoff/frontier/recovery/handoff affordance. - -Incoming: - motivated_by G2: avoid fake closure and stranded users - constrained_by C8: no generic task-planning surface - derived_from D94: phase progression is frontier-anchored - -Outgoing: - verified_by K13: open phases bottom-load one visible artifact - protected_by I24: stream projection/hydration stability - refined_by R18: open interview phases default to kickoff/frontier/generation/recovery -``` - -This is a stronger context object than "all goals, all constraints, all requirements." It lets the interviewer and observer reason about consequences, gaps, and drift. - -The `edge-local neighborhood` Lexicon entry in `memory/SPEC.md` already names this pattern; this section gives it concrete shape. - -## Topology-driven question ranking - -Once the graph carries kinds, subtypes, and typed edges, the interviewer can rank next questions by graph topology rather than by template. - -Heuristics worth implementing first: - -| Signal | Suggested question shape | -| --- | --- | -| `assumption` with high fanout (many depends_on edges out) and low confidence | "We're depending on the assumption that X. Do you want to validate it?" | -| `requirement` with no `verifies` incoming | "How will we know this requirement holds?" | -| `criterion` with no `verifies` outgoing target | "What does this criterion check? Which requirement or invariant?" | -| `decision` with no `rejected_alternatives` | "What did we consider and rule out before choosing this?" | -| Conflicting `constrains` edges into the same target | "These two constraints disagree about X. Which wins?" | -| `goal` with no derived requirements | "We've stated this goal but nothing in the spec ties to it. What would satisfy it?" | -| `requirement` with no `examples` and high external uncertainty | "What's a concrete case where this requirement would matter?" | - -These heuristics complement the behavioral-kernel signal-phrase routing in [`BEHAVIORAL_KERNELS.md`](./BEHAVIORAL_KERNELS.md): kernels suggest *what kind* of question to ask; topology heuristics suggest *which item* to ask about next. - -## Translation table - -A useful contract for the observer and the interviewer: which user phrases map to which kind. This is the bridge between user vocabulary and ontology. - -| User phrase pattern | Most likely kind | -| --- | --- | -| "always true that…" | `invariant` (state subtype) | -| "should never…" | `invariant` (state or transition) | -| "valid transition from X to Y" | `invariant` (transition) | -| "invalid input" | `criterion` (runtime_check) or `invariant` (data_integrity) | -| "for example, when…" | `example` (positive) | -| "but what about the case where…" | `example` (edge_case) | -| "we wouldn't want…" | `example` (negative / counterexample) or `constraint` | -| "another plausible interpretation is…" | `example` (disambiguates) | -| "if this happened it would be a serious bug" | `criterion` (high-priority verification target) | -| "we don't care about X" | `constraint` (non_goal) | -| "we picked Y over Z because…" | `decision` | - -The observer should treat these as **strong priors**, not rigid rules. The classification rule above still governs final assignment. - -## Progressive checkability binding - -Every claim carries a `checkability` field describing the strongest oracle that currently witnesses it. The ladder, from weakest to strongest: - -``` -1. human_review — a person reads it and judges -2. example — a concrete witness (positive) -3. counterexample — a concrete case ruled out (negative) -4. regression_test — an automated test -5. runtime_contract — a runtime assertion / pre/post condition -6. state_machine_rule — a state or transition constraint enforced by the model -7. invariant — a property model-checked over reachable states -8. proof_obligation — a static proof in a verifier -``` - -Plus an explicit step beneath the ladder: `unresolved_ambiguity` — claims that are intentionally open. - -The discipline is: **emit the weakest sufficient artifact for the claim at hand.** Some claims need only examples. Some deserve runtime assertions or property tests. Some should remain qualitative, but they should be marked honestly rather than laundered into fake precision. - -A claim's record carries: - -```ts -type Checkability = - | 'human_review' - | 'example' - | 'counterexample' - | 'regression_test' - | 'runtime_contract' - | 'state_machine_rule' - | 'invariant' - | 'proof_obligation' - | 'unresolved_ambiguity' - -type ClaimMetadata = { - checkability: Checkability - oracle?: string // path to the test, contract, or proof - strength: 'asserted' | 'example_backed' | 'tested' | 'enforced' | 'proved' - validTraces?: string[] - invalidTraces?: string[] -} -``` - -The `strength` field forces honesty: "checked on three examples" is not the same claim as "proved for all reachable states." A claim's `checkability` says *what kind* of witness exists; `strength` says *how broad* that witness is. - -## Consumers of the typed graph - -This ontology is the substrate for several near-term capabilities: - -| Capability | Uses | -| --- | --- | -| Observer relation-first capture (existing, FE-639) | Kinds, edge schema, support/status, relation-policy registry | -| Cascade preview (existing, A48) | `cascade` axis on relation-policy registry | -| Reconciliation needs (active, Multi-chat substrate) | `reconciliation` axis; status transitions | -| Behavioral kernels (planned, FE-702 probes) | Kernel signals consume kinds and edges; emit invariants, examples, criteria | -| Candidate-spec assist (horizon) | Generates batches of typed claims with declared support and rationale | -| Architect / generator loop (horizon) | Same, plus proposes edges; HITL review through reconciliation | -| Spec drift detection (proposed) | Compares claim's `checkability` and `strength` to evidence in implementation | -| Export grounding (existing) | Uses `export_trace` axis to explain why each requirement is in the export | -| Topology-driven question ranking (proposed) | Reads kind + edge density + epistemic metadata to suggest next questions | - -## Open questions - -- **`Property` as a shared primitive.** The synthesis proposes a `Property` record that requirements *commit to* and criteria *observe*, factoring out a many-to-many relationship instead of pairing them by paraphrase. Worth prototyping but not committed; the current document treats requirements and criteria as directly linked through `verifies`. (See `memory/SPEC.md` Lexicon entry for `property *(candidate ontology)*`.) -- **Subtypes vs. top-level kinds.** Have we picked the right split? `non_goal` could be its own top-level kind rather than a constraint subtype. The argument against is that nine top-level kinds is already at the edge of what users can hold in their heads. -- **Edge support thresholds.** When does `weak_candidate` become `strong_inference`? Should this be a number of corroborating signals, an LLM-emitted confidence, or a human review? -- **Relation-policy registry granularity.** One relation, all kinds vs. one relation per source-kind / target-kind pair? The latter is more precise but explodes combinatorially. -- **Migration of existing edges.** The current schema's edges have no `support`, `status`, `family`, or `rationale` field. Backfill to `support: explicit, status: accepted` for existing edges, or treat them all as `strong_inference` until reviewed? -- **Where the `Checkability` field actually lives.** On the claim itself (denormalized), on a `verification` join table, or both? -- **Observer abstention thresholds.** What classification confidence is needed to emit a kind? Today's observer is conservative; the typed ontology may let it be more confident in some cases (clear "should never" → invariant) and less confident in others (decision-capture criteria are strict). - -## References - -- [`INTENT_SPEC_EVOLUTION.md`](../archive/design/INTENT_SPEC_EVOLUTION.md) §3 (shared claims), §4 (knowledge edges), §6 (ambiguity-targeted disambiguation), §11 (persistence model). -- [`BEHAVIORAL_KERNELS.md`](./BEHAVIORAL_KERNELS.md) — kernels generate the questions; this document defines what their answers become. -- `memory/SPEC.md` Requirement 38 (invariant + example as kinds), Requirement 30 (relation-first observer), I109 (compact existing-knowledge anchors), Lexicon entries for `intent graph`, `progressive checkability`, `behavioral kernel`, `edge-local neighborhood`, `property *(candidate)*`, `invariant *(planned)*`, `example *(planned)*`. -- `memory/PLAN.md` item 3 (FE-700) — the active frontier item this document expands. diff --git a/archive/docs/design/MULTI_CHAT.md b/archive/docs/design/MULTI_CHAT.md deleted file mode 100644 index 874eb45ac..000000000 --- a/archive/docs/design/MULTI_CHAT.md +++ /dev/null @@ -1,386 +0,0 @@ -# Multi-Chat Substrate — Design Spec - -> Output of brainstorm session 2026-05-05 with Lu. First phase of the larger intent-graph evolution now captured in `memory/SPEC.md` as the split between conversational turn history, current intent-graph truth, reconciliation needs, and future semantic changesets / changes. Substrate-only: data model, relationships, migrations. Reconciliation-agent loop, side-chat UI changes, and the full changeset ledger are deliberately out of scope. -> -> Status: **shipped substrate reference** — the Phase 1 `chat` / `turn.chat_id` / `specification.primary_chat_id` / `reconciliation_need` substrate has landed. Use this document for schema rationale and migration invariants; use [CONVERSATIONAL_WORKSPACE_RUNTIME.md](./CONVERSATIONAL_WORKSPACE_RUNTIME.md) for the consolidated future runtime concept. -> -> Relationship to side-chat/runtime design: this document superseded older side-chat substrate assumptions for Phase 1. Future thread hierarchy, persistent side-chat history, and reconciliation-in-stream decisions are folded into the conversational workspace runtime synthesis. - -## How to read this after Phase 1 shipped - -This document is now a substrate reference. It owns Phase 1 schema rationale and compatibility invariants, not the future runtime UX. - -| Claim area | Current reading | -|---|---| -| `chat`, nullable `turn.chat_id`, `specification.primary_chat_id`, mirrored `chat.active_turn_id` | Shipped Phase 1 substrate and migration rationale. | -| `reconciliation_need` | Shipped process-debt primitive. Future cause/resolution provenance should use changeset vocabulary, not patch vocabulary. | -| Side-chat persistence / Phase 2 | Historical substrate option. Current runtime synthesis may persist side conversations as child chats, a new thread table, or UI-rendered threads. | -| Reconciliation agent / Phase 3 | Historical staging description. Current target is a reconciliation runtime/thread that uses V3.1 classifier outputs and future changeset attribution. | -| Patch ledger / Phase 4 | Historical name. Current plan calls this the semantic changeset ledger. | -| Context model for new chats | Still useful principle: new chats consume current graph state, not transcript snapshots. Thread-scoped context details now belong to `CONVERSATIONAL_WORKSPACE_RUNTIME.md`. | - -## 1. Concept & problem - -Today every turn anchors directly to a `specification`, and a single linear turn chain *is* the spec's history spine: - -- `turn.specification_id` is the only home for a turn. -- `turn.parent_turn_id` chains turns into one rope. -- `specification.active_turn_id` names the head of that rope. - -This was correct when there was one interview thread per spec. It is no longer correct: - -- **Side-chat** needs a parallel conversation surface anchored to graph items, not to the interview frontier. Early UI slices shipped this through an in-memory patch-list surface; the substrate now supports separate chats while the future runtime decides thread shape. -- **Direct user edits** from graph view (and, later, the architect loop) produce mutations that don't originate from any turn at all — they need a place to live and a way to advertise their downstream impact. -- **Reconciliation** of those mutations needs a typed signal: "this item changed, that item now needs confirmation". `knowledge_edge` carries semantic relations between items; it is the wrong place to record an open question between them. - -This RFC introduced the smallest substrate change that unblocked both: a `chat` table that turns can relate to, and a `reconciliation_need` table that records directed open issues between graph targets. - -It is **Phase 1** of the substrate evolution leading toward the changeset ledger and ontology sharpening discussed in `memory/SPEC.md` decisions D134-D138. Subsequent substrate phases in §10 are historical staging, not current sequencing authority. Adjacent moves not part of this evolution — phase-route de-emphasis, changesets with `before_json` / `after_json` provenance, ontology additions (`invariant`, `example`) — are tracked separately. - -### At a glance — the relational shift - -```mermaid -flowchart LR - subgraph Today - S1[specification] -- "1..*" --> T1[turn] - T1 -.->|parent_turn_id| T1 - S1 -- "active_turn_id" --> T1 - S1 -- "1..*" --> KI1[knowledge_item] - KI1 <-- "from / to" --> KE1[knowledge_edge] - T1 <-- "turn_knowledge_item" --> KI1 - end - - subgraph Proposed - S2[specification] -- "1..*" --> C2[chat] - S2 -- "primary_chat_id" --> C2 - C2 -- "1..*" --> T2[turn] - T2 -.->|parent_turn_id| T2 - C2 -- "active_turn_id" --> T2 - S2 -- "1..*" --> KI2[knowledge_item] - KI2 <-- "from / to" --> KE2[knowledge_edge] - KI2 <-- "source / target" --> RN2[reconciliation_need] - T2 <-- "turn_knowledge_item" --> KI2 - end -``` - -Phase 1 adds two new tables (`chat`, `reconciliation_need`), a nullable `turn.chat_id`, a `specification.primary_chat_id`, and a mirrored `chat.active_turn_id`. Legacy `turn.specification_id` and `specification.active_turn_id` stay during the transition; the clean end state is still that turns belong canonically to chats and active turn heads live on chats. - -## 2. Current model (annotated) - -``` -specification - id - name - mode 'greenfield' | 'brownfield' - active_turn_id head of the single turn chain ← mirrored to chat in Phase 1, later moves - created_at, updated_at - -turn - id - specification_id every turn anchors here ← retained in Phase 1, later replaced by chat_id - parent_turn_id chains turns - phase 'grounding' | 'design' | 'requirements' | 'criteria' - turn_kind 'question' | 'kickoff' | 'recovery' - question, why, impact, answer, ... - is_resolution - user_parts, assistant_parts - created_at - -option - id - turn_id 1:N from turn (unchanged in V1; see §9) - position, content, is_recommended, is_selected - -phase_outcome - id - specification_id spec-level (unchanged) - phase - proposal_turn_id turns still own the moments - confirmation_turn_id - status, summary, closure_basis, confirmed_at, superseded_at - created_at - -knowledge_item - id - specification_id (unchanged) - kind 'goal' | 'term' | 'context' | 'constraint' - | 'decision' | 'assumption' | 'requirement' | 'criterion' - subtype, content, rationale, kind_ordinal - -turn_knowledge_item (unchanged) - turn_id, item_id - relation 'captured' | 'confirmed' | 'edited' - | 'invalidated' | 'reviewed' | 'rejected' - -knowledge_edge (unchanged) - from_item_id, to_item_id - relation 'depends_on' | 'derived_from' - | 'constrains' | 'verifies' | 'refines' - -annotation (unchanged) - id, specification_id, knowledge_item_id - summary, body, selection_start, selection_end, created_at -``` - -The fragility: `turn.specification_id` plus `specification.active_turn_id` plus `parent_turn_id` collectively encode "one rope per spec". Any new conversation surface bumps into this triple. - -## 3. Proposed model — minimum changes - -Two new tables, one new nullable FK, one mirrored active-head column, and one column added on spec. Names are placeholders; the conventions match the existing schema. Phase 1 is deliberately softer than the end state so the implementation can land without rewriting every spec-scoped query at once. - -### 3.1 `chat` (new) - -```ts -export const chat = sqliteTable( - 'chat', - { - id: integer().primaryKey({ autoIncrement: true }), - specification_id: integer() - .notNull() - .references(() => specification.id), - kind: text({ enum: ['interview', 'side_chat'] }).notNull(), - active_turn_id: integer().references((): any => turn.id), - created_at: text().notNull().default(sql`(datetime('now'))`), - }, - (table) => [ - uniqueIndex('chat_interview_one_per_spec') - .on(table.specification_id) - .where(sql`kind = 'interview'`), - ], -); -``` - -- `kind` distinguishes the canonical interview chat (one per spec, today) from side-chats (zero or more per spec). Future kinds (`architect`, `revisit`, …) extend this enum. -- `active_turn_id` moves off `specification` and onto `chat`. Each chat has its own head. -- A spec invariant emerges: every spec has exactly one `chat` with `kind = 'interview'`. The partial unique index enforces *at most one* interview chat per spec; spec creation and migration code enforce *at least one* by creating the spec and its interview chat in one transactional unit and setting `specification.primary_chat_id`. - -### 3.2 `turn` (changed) - -```ts -export const turn = sqliteTable('turn', { - id: integer().primaryKey({ autoIncrement: true }), - specification_id: integer() - .notNull() - .references(() => specification.id), // retained during Phase 1 - chat_id: integer() // ← new Phase 1 pointer - .references(() => chat.id), - parent_turn_id: integer().references((): any => turn.id), - phase: text({ enum: ['grounding', 'design', 'requirements', 'criteria'] }).notNull(), - turn_kind: text({ enum: ['question', 'kickoff', 'recovery'] }) - .notNull() - .default('question'), - // ... rest unchanged -}); -``` - -- `specification_id` stays during Phase 1 as compatibility and query convenience. New writes should also set `chat_id`, and application assertions keep `turn.specification_id === chat.specification_id`. -- The end state is that `specification_id` can be dropped and spec becomes reachable via `chat.specification_id`, but that is not required in the first slice. -- `phase` stays on turn. Per the second meeting: phase remains a background signal that shapes agent prompting; only the *UI primacy* of phase is being de-emphasised, and that's a separate RFC. -- `parent_turn_id` keeps its current semantics. It is still scoped to a single chat (parent and child must share `chat_id`); side-chat turns chain inside the side-chat, interview turns chain inside the interview chat. - -### 3.3 `specification` (changed) - -```ts -export const specification = sqliteTable('specification', { - id: integer().primaryKey({ autoIncrement: true }), - name: text().notNull(), - mode: text('mode', { enum: ['greenfield', 'brownfield'] }).notNull().default('greenfield'), - primary_chat_id: integer().references(() => chat.id), // ← new - active_turn_id: integer().references((): any => turn.id), // retained during Phase 1 - created_at: text().notNull().default(sql`(datetime('now'))`), - updated_at: text().notNull().default(sql`(datetime('now'))`), -}); -``` - -- `primary_chat_id` names the canonical interview chat. Today this is "the chat", tomorrow it's "the rope alongside which side-chats hang". Code that wants the interview frontier reads `specification → primary_chat_id → active_turn_id`. -- No `active_chat_id` is added in Phase 1. The only active head needed for this slice is the primary interview chat's `active_turn_id`; a separate active-chat pointer should wait until the UI has multiple simultaneously navigable chat surfaces and a clear synchronization rule. -- `active_turn_id` on the spec remains during Phase 1. Once read/write paths are stable through `primary_chat_id → active_turn_id`, the spec-level pointer can be dropped in a later cleanup migration. - -### 3.4 `reconciliation_need` (new) - -```ts -export const reconciliationNeed = sqliteTable( - 'reconciliation_need', - { - id: integer().primaryKey({ autoIncrement: true }), - specification_id: integer() - .notNull() - .references(() => specification.id), - source_item_id: integer() - .notNull() - .references(() => knowledgeItem.id, { onDelete: 'cascade' }), - target_item_id: integer() - .notNull() - .references(() => knowledgeItem.id, { onDelete: 'cascade' }), - kind: text({ enum: ['supersedes', 'needs_confirmation'] }).notNull(), - status: text({ enum: ['open', 'resolved'] }).notNull().default('open'), - reason: text(), - caused_by_turn_id: integer().references(() => turn.id), - caused_by_patch_id: integer(), // historical placeholder; current concept is caused_by_changeset_id - created_at: text().notNull().default(sql`(datetime('now'))`), - resolved_at: text(), - }, - (table) => [ - // Multiple needs between the same items are allowed, but only one OPEN need - // per (source, target, kind) — re-firing on an already-open issue is a no-op. - uniqueIndex('reconciliation_need_open_unique') - .on(table.source_item_id, table.target_item_id, table.kind) - .where(sql`status = 'open'`), - ], -); -``` - -- **Directional.** `source` is the item whose change *triggered* the issue; `target` is the item that may now need attention. The pair (`source`, `target`, `kind`) is the issue identity. -- **Spec-local.** `specification_id`, `source_item_id`, and `target_item_id` must all point into the same spec. SQLite only enforces the direct FKs shown above, so insertion and migration code must validate `source.specification_id = target.specification_id = reconciliation_edge.specification_id` before writing. -- **Kinds.** Two ship at Phase 1: - - `supersedes` — the source change replaces or invalidates information the target depends on; target needs to be re-derived or marked stale. - - `needs_confirmation` — the source change *might* affect target but the system can't decide deterministically; a human or agent has to look. - - The enum is intentionally narrow. New kinds are added when we have a concrete reconciliation move that doesn't fit either; we don't pre-invent them. -- **Status lifecycle.** `open` on creation; `resolved` on agent / user action. Resolved needs are kept for audit but do not participate in the reconciliation queue. -- **Multiple needs per pair.** The unique index gates only `open` needs. Two successive edits to the same source can fire two `needs_confirmation` needs, the first being closed before the second is opened; what we forbid is *two simultaneously-open issues of the same kind for the same pair*. -- **Provenance.** Phase 1 carries `reason`, `caused_by_turn_id`, and nullable historical `caused_by_patch_id`. The turn pointer is useful immediately for observer / review-created needs; the semantic-mutation pointer is a deliberate placeholder that stays null until the changeset ledger gives every semantic mutation a stable id. - -### 3.5 Everything else - -`option`, `phase_outcome`, `knowledge_item`, `turn_knowledge_item`, `knowledge_edge`, `annotation` are **untouched**. Their relationships continue to work because turn ids remain stable across the migration; only what `turn` references upward changes. See §6. - -## 4. Context model for new chats - -Substrate-only note: this RFC does **not** specify the assembly logic, the prompt format, or who orchestrates it. It only specifies what data is reachable to a new chat at creation time. - -A side-chat (or any non-interview chat) is created with: - -- `chat.specification_id` — handle on the spec it lives in. -- `chat.kind` — distinguishes side-chat from interview. -- *No* link to a "parent" chat. Chats are siblings under a spec, not a tree. -- *No* automatic snapshot of the interview transcript. The data the side-chat consumes is the **current state of the spec**: - - all `knowledge_item` rows for the spec, - - all `knowledge_edge` rows between them, - - all open `reconciliation_need` rows, - - `phase_outcome` history (which phases are open / confirmed), - - spec metadata (`name`, `mode`). - -This is the second meeting's explicit decision: *new chats take in the current knowledge graph rather than previous conversation turns*. The interview transcript is provenance, not context. - -How that gets formatted into a prompt and which agent owns the assembly is now part of the runtime/context-provision track in [CONVERSATIONAL_WORKSPACE_RUNTIME.md](./CONVERSATIONAL_WORKSPACE_RUNTIME.md). - -## 5. Reconciliation primitive - -Substrate-only note: this RFC describes the **edge model and lifecycle**. The reconciliation runtime that reads the queue, classifies severity, presents threads/review affordances, and applies changeset-backed resolutions is owned by [CONVERSATIONAL_WORKSPACE_RUNTIME.md](./CONVERSATIONAL_WORKSPACE_RUNTIME.md). - -### 5.1 Two production paths - -```mermaid -flowchart TD - M[Knowledge item changes
(direct edit, changeset apply,
review acceptance)] --> P1[Path 1: deterministic] - M --> P2[Path 2: observer pass] - P1 --> KE[Look up existing
knowledge_edges
(depends_on, derived_from,
constrains, refines, verifies)] - KE --> RE1[Insert reconciliation_need
per affected pair
kind = 'supersedes' / 'needs_confirmation'] - P2 --> OB[Observer reads
changed item + neighbourhood] - OB --> NEW[Discovers new connection
not previously in graph] - NEW --> RE2[Insert reconciliation_need
(may also insert new
knowledge_edge)] - - classDef change fill:#fef3c7,stroke:#d97706 - classDef path fill:#dbeafe,stroke:#2563eb - classDef out fill:#fed7aa,stroke:#ea580c - class M change - class P1,P2,KE,OB,NEW path - class RE1,RE2 out -``` - -- **Path 1 (deterministic).** When an item changes, the system enumerates outgoing `knowledge_edge`s where it is the source (or incoming, depending on relation semantics — owned by the reconciliation agent's policy, not this RFC). For each, it opens a `reconciliation_need`. This is the only path Phase 1 needs to ship. -- **Path 2 (observer pass).** Asynchronous. The observer surveys the change in context of the current graph and may notice that two items now look related where they weren't before. It can insert both a new `knowledge_edge` (the discovered relation) and an `open` `reconciliation_need` (the issue raised by the discovery). This path is the one that handles the case from the meeting: *"now I'm rewriting the decision in a way that now sounds like I'm assuming the other decision as well"*. Substrate is ready for it; the observer prompt isn't. - -### 5.2 Resolution - -When the queue is resolved (by user, agent, or both), the matching `reconciliation_need` rows transition `open → resolved` and pick up a `resolved_at` timestamp. The actual resolution moves — accept a proposed changeset, edit the target item, mark the issue irrelevant — produce knowledge-item mutations and, once FE-701 lands, changesets. Those are not modelled here; they go through the same paths everything else does. - -### 5.3 What this is *not* - -- Not a workflow state. Reconciliation is a graph signal, not a phase. `phase_outcome` is the workflow state primitive and is unchanged. -- Not a changeset. `reconciliation_need` records *that* an issue exists; it does not describe *what* should change. The proposed change is a separate artifact: historically in-memory in the patch-list UI, durably in the changeset ledger when it lands. -- Not an audit log of edits. `turn_knowledge_item` and (later) the changeset ledger own that. - -## 6. Migration - -Drizzle / SQLite. One ordered migration, columns added before the dependent columns are dropped: - -1. **0014_chat_table.sql** - - Create `chat` table (id, specification_id, kind, active_turn_id nullable, created_at). - - For each existing spec, insert one row: `kind = 'interview'`, `active_turn_id = specification.active_turn_id`. -2. **0015_turn_chat_id.sql** - - Add `turn.chat_id` (nullable). - - Backfill: for each turn, set `chat_id` to the interview chat of its current `specification_id`. - - Make `chat.active_turn_id` `NOT NULL` for `kind = 'interview'` rows (handled in code; SQLite doesn't enforce partial NOT NULL). -3. **0016_specification_primary_chat.sql** - - Add `specification.primary_chat_id`. - - Backfill: for each spec, set `primary_chat_id` to the interview chat created in step 1. -4. **0017_reconciliation_need.sql** - - Create `reconciliation_need` table with the partial unique index from §3.4. - - Include `caused_by_turn_id` now and nullable historical `caused_by_patch_id` as a future changeset-ledger placeholder. - -Code changes paired with migrations: - -- Reads that need the interview head may move from `specification.active_turn_id` toward `specification → primary_chat_id → active_turn_id` incrementally. -- Writes that previously inserted a turn with `specification_id` now also insert with `chat_id` (the interview chat for the spec, or whichever chat the caller owns). -- Application assertions ensure `turn.specification_id === chat.specification_id` and `parent_turn_id` stays inside the same chat. -- Spec creation today inserts a spec; tomorrow it inserts a spec **and** an interview chat as one transactional unit. `bin/brunch new-spec` and the equivalent route handler are the only entry points. - -Legacy cleanup is deferred: - -The recreate-and-copy steps must preserve the FK graph, not just the column data. `turn.id` and `specification.id` stay stable while rebuilding the tables, and the replacement tables must recreate every surviving FK/index that references them (`turn.parent_turn_id`, `chat.active_turn_id`, `option.turn_id`, `phase_outcome.proposal_turn_id`, `phase_outcome.confirmation_turn_id`, `turn_knowledge_item.turn_id`, and `specification.primary_chat_id`). Each step runs inside a transaction with a pre/post `PRAGMA foreign_key_check` so transient FK breakage is caught before the migration commits. - -No data loss. Every existing turn lands inside the interview chat of its spec; every existing `specification.active_turn_id` becomes the interview chat's `active_turn_id`. `phase_outcome`, `option`, `knowledge_item`, `turn_knowledge_item`, `knowledge_edge`, `annotation` are untouched. - -## 7. Out of scope (acknowledged adjacents) - -- **Changeset ledger.** Typed semantic changesets with before/after values and explicit provenance, replacing the in-memory patch-list model. This RFC creates room for the ledger by separating chat from spec, but does not introduce the ledger itself. -- **Phase routes / phase as primary UI concept.** The second meeting agreed phase should de-emphasise as UI but stay as a background signal for prompting. UI work is its own RFC; the data model here keeps `turn.phase` exactly as-is. -- **Ontology sharpening (`invariant`, `example` as `knowledge_item.kind`).** Discussed in `memory/SPEC.md` D134 and D136. Pure ontology change; no impact on the chat / reconciliation substrate. -- **Decision shape rework.** The meeting concluded a decision should capture both *chosen* and *not chosen*, and that the `option` table can probably go away in favour of in-turn data. Both moves belong with changeset-ledger / decision-shape work; today's `option` table stays. -- **Phase outcome enum redesign.** The meeting flagged the `proposed | confirmed | superseded` enum as "find a better idea". Out of scope; `phase_outcome` is unchanged. -- **Reconciliation runtime.** Who reads `reconciliation_need` rows, in what order, how it presents review affordances, and how accepted resolutions become changesets. Substrate is ready; the runtime design is in `CONVERSATIONAL_WORKSPACE_RUNTIME.md`. -- **Side-chat UI changes for multi-thread.** Historical UI could ship a single side-chat-per-spec through an in-memory patch-list surface; the `chat` table accommodates many but the future user surface may be child chats, a thread table, or UI-rendered in-stream threads. User-surface version labels from older UI design docs are independent of substrate Phase 1 / 2 / 3 / 4 — see §10 for the historical mapping. - -## 8. Verification stance - -Substrate change. Coverage is migration-and-FK shaped, not user-flow shaped: - -| Loop | Coverage | -|---|---| -| **Schema migration tests** | Forward migration produces one `chat` per spec; every existing turn has a matching `chat_id`; every spec has a matching `primary_chat_id`; chat's `active_turn_id` matches old spec's. Idempotent on re-run. | -| **FK integrity tests** | Inserting a turn with a `chat_id` whose chat belongs to a different spec is rejected at the application layer (DB enforces only direct FK). Parent / child turns must share `chat_id` (application-layer assertion). SQLite table-recreate migrations preserve and revalidate all surviving turn/spec FKs. | -| **Reconciliation lifecycle tests** | Opening a duplicate `(source, target, kind)` while one is `open` is rejected. Resolving and re-opening with the same triple is allowed. Cross-spec source/target pairs are rejected at the application layer. Cascade-delete on `knowledge_item.id` removes both knowledge edges and reconciliation edges. | -| **Read-path regression** | `specification → primary_chat_id → active_turn_id` returns the same turn id that `specification.active_turn_id` did pre-migration on a fixture set. Legacy spec-head reads and new chat-head reads agree during transition. | -| **Existing turn-knowledge-item provenance** | Survives migration unchanged. Every `turn_knowledge_item` row continues to point to a valid turn. | - -Manual: spin up an existing spec database (a current `.brunch/` fixture), run migrations, exercise the existing interview flow, confirm no behavioural change. - -## 9. Open questions - -- **`turn.specification_id` retention.** Phase 1 intentionally keeps it as a softer migration: existing spec-scoped reads keep working while new writes populate `chat_id` and assertions prove both pointers agree. The end-state cleanup should drop it once hot paths and tests read ownership through `chat_id`, unless profiling proves the denormalized field pays for itself. -- **Side-chat/thread focus.** A side conversation is started *from* a graph item. Should focus live on `chat`, a later `chat_focus` table, a new thread table, or thread-context rows? Historical default: don't model it on `chat`; current runtime synthesis leaves this to the thread/context substrate decision. -- **Reconciliation `reason` shape.** Free text in V1. Once the reconciliation agent ships, `reason` may want to be structured (template id + slots). Default proposal: stay free-text until the agent design forces a shape. -- **Reconciliation cascade-on-resolve.** When a `supersedes` need resolves, does that ever fan out into new reconciliation needs (because the resolution itself is a mutation)? Yes — and that is exactly the reentrancy point Lu flagged in the second meeting. Substrate already handles it: any mutation re-runs path 1 + path 2. The agent decides whether to bundle resolution into one review set or accept a follow-up cycle. No substrate change needed. -- **`option` table fate.** Meeting tentatively concluded the table can go away in favour of in-turn data. Out of scope here; tracked alongside changeset-ledger / decision-shape work. -- **`phase_outcome` enum redesign.** Tracked alongside the de-emphasise-phase-as-UI RFC. -- **Multiple `reconciliation_need.kind`s for one pair.** The partial unique index gates only same-kind same-direction. A single source change could legitimately produce both `supersedes` *and* `needs_confirmation` against the same target; allowed by design. Confirm this is intended. - -## 10. Phasing - -`Phase N` labels here describe the *substrate* evolution and are independent of any V1 / V2 / V3 / V4 labels used for the *user surface*. The `Enables` column maps between them where they connect. - -| Phase | Substrate state | Enables (user-surface mapping) | -|---|---|---| -| **Phase 1** *(this RFC; shipped)* | `chat` table; nullable `turn.chat_id`; `specification.primary_chat_id`; mirrored `chat.active_turn_id`; `reconciliation_need` table with lightweight provenance placeholders. Backfill migrations. New writes populate both legacy and chat pointers. No user-visible change: still one chat per spec, still one rope per chat, side-chat can continue to use an in-memory patch-list surface. | Foundation. Existing side-chat / graph-edit surfaces can ship against today's mutation paths regardless of order. Hard-edit cascade gets a clean reshape once it reads from `reconciliation_need` rather than ad-hoc REVISIT state. Persistent multi-thread side-chat and the architect loop become shippable without waiting on the full changeset ledger. | -| **Phase 2** *(historical substrate option)* | Side-chat persistence: side-chat threads write `chat` rows with `kind = 'side_chat'` and persist their turns. Multiple side-chats per spec become possible at the data layer. | Persistent side-chat history and old-thread UI could activate, unless the runtime track chooses child chats, a separate thread table, or UI-rendered threads. | -| **Phase 3** *(historical staging)* | Reconciliation agent loop reads `reconciliation_need` queue, presents review sets through the same patch-list-style surface as the side-chat. | V3.1 has shipped classifier output in the Pending review bridge surface; the future target is a reconciliation thread in the unified runtime. | -| **Phase 4** *(later; current name FE-701 changeset ledger)* | Changeset ledger lands. Reconciliation needs gain changeset-backed cause/resolution provenance. Decision-shape rework, option-table removal, and phase-outcome enum redesign may happen here or in adjacent slices. | Architect-loop proposals, item versioning, and cross-surface undo / time-travel become possible through changeset history. | - -## 11. Traceability - -- **Replaces** the implicit "one rope per spec" assumption baked into `turn.specification_id` and `specification.active_turn_id`. -- **Unblocks** the changeset ledger, the architect / generator loop horizon item, and persistent multi-chat / thread history. -- **Bounded by** D113 (no second durable workflow model — `chat` is *not* workflow state, it is a conversation-thread substrate; workflow state stays on `phase_outcome`). -- **Reuses** existing `knowledge_item`, `knowledge_edge`, `turn_knowledge_item`, `option`, `phase_outcome`, `annotation` schemas as-is. -- **References** `memory/SPEC.md` decisions D135, D137, and D138 plus `docs/design/PATCH_LEDGER.md` for deeper semantic mutation history pressure. Supersedes older side-chat substrate assumptions while remaining compatible with the user-facing side-chat surface. diff --git a/archive/docs/design/PATCH_LEDGER.md b/archive/docs/design/PATCH_LEDGER.md deleted file mode 100644 index 357a643b0..000000000 --- a/archive/docs/design/PATCH_LEDGER.md +++ /dev/null @@ -1,753 +0,0 @@ -# Patch Ledger and Reconciliation - -> Status: **historical design pressure** — retained for semantic mutation history, reconciliation bases, target ordering, and phase-two ledger rationale. Future-facing schema and operation vocabulary is **changeset/change**, not patch/patch_change; the consolidated runtime concept lives in [CONVERSATIONAL_WORKSPACE_RUNTIME.md](./CONVERSATIONAL_WORKSPACE_RUNTIME.md). -> Date: 2026-05-05. -> Scope: Brunch runtime product persistence, not the file-backed development registry explored elsewhere. - -## How to read this after the changeset vocabulary shift - -This document predates the final vocabulary choice. Treat it as an algorithm and rationale source, not as a naming authority. - -| Historical wording here | Current wording / authority | -|---|---| -| `patch` | `changeset` — one atomic semantic mutation bundle. | -| `patch_change` | `change` — one atomic operation inside a changeset. | -| `caused_by_patch_id`, `resolved_by_patch_id` | Future changeset-backed cause/resolution fields; final column names should be chosen by the FE-701 changeset-ledger design. | -| Patch list / reconciliation review set | Historical review-surface framing. Current runtime synthesis routes proposals through proposal turns and accepted changesets. | -| Target ordering and reconciliation bases | Still useful algorithmic pressure. Preserve these concepts when implementing reconciliation threads or graph-review repairs. | - -Do not introduce new schema, capability contracts, or operation ids with `patch` / `patch_change` unless deliberately referring to this historical design. - -## Why this note exists - -Brunch is moving from a single interview transcript toward an intent-graph workspace. A specification can now plausibly include: - -- a primary guided interview -- side chats focused on one item or graph neighborhood -- observer captures from answered turns -- user-directed graph edits -- review-set accept / request-changes flows -- verifier or implementation feedback -- reconciliation passes after upstream meaning changes - -The current persistence model still treats `turn` as the main historical spine: turns belong directly to a `specification`, and knowledge items are linked back to turns through `turn_knowledge_item`. - -That works for an interview-led product, but it becomes strained once semantic changes can originate outside the primary conversation. The proposal here is to separate three authorities. The original wording used `patch`; current canonical vocabulary uses `changeset` / `change` for that middle authority: - -```text -chat / turn: - conversational provenance and replay - -changeset / change: - semantic mutation history for the intent graph - -reconciliation_need: - semantic debt created when a change may affect existing graph truth -``` - -The intent graph remains the current semantic truth. The changeset ledger records how that truth changed. Reconciliation records what may now need renewed judgment. - -## Current Shape - -The current schema has these relevant tables: - -```text -specification - id - active_turn_id - -turn - id - specification_id - parent_turn_id - phase - turn_kind - user_parts - assistant_parts - -knowledge_item - id - specification_id - kind - content - rationale - -turn_knowledge_item - turn_id - item_id - relation - -knowledge_edge - from_item_id - to_item_id - relation -``` - -This means: - -- one specification can have many turns, but no durable chat container -- turn ancestry is global inside a specification rather than scoped to a chat -- knowledge provenance is turn-centered -- semantic dependencies live in `knowledge_edge` -- there is no durable representation of semantic mutations as first-class events -- there is no durable representation of pending semantic reconciliation - -## Proposed Concepts - -`docs/design/MULTI_CHAT.md` is now the concrete phase-one substrate reference for chat containers and reconciliation needs. This document remains the deeper design pressure for future semantic mutation history, richer reconciliation targeting, ordering, and changeset-backed provenance. - -### Chat - -A `chat` is a conversation container inside a specification. - -It should not own semantic truth. It owns conversational context, transcript replay, and local interaction focus. - -Examples: - -- the primary interview chat -- a side chat about one knowledge item -- a side chat about a graph neighborhood -- a reconciliation chat for an open set of reconciliation needs -- a verifier or implementation feedback chat - -Proposed table: - -```text -chat - id - specification_id - kind - title - status - created_at - updated_at -``` - -The concrete phase-one `chat` shape is intentionally narrower in [Multi-Chat Substrate](./MULTI_CHAT.md): add `chat`, nullable `turn.chat_id`, `specification.primary_chat_id`, and mirrored `chat.active_turn_id` while keeping legacy `turn.specification_id` and `specification.active_turn_id` during transition. The richer fields above are possible later extensions, not requirements for the first slice. - -Suggested enums: - -```text -kind: - primary - side - reconciliation - review - verifier - -status: - active - archived -``` - -The schema should support a primary chat, but should not require the product model to have exactly one primary chat. A specification can have many chats, and those chats can all produce semantic mutations. "Primary" is a useful label for today's interview-led flow, not a permanent ontology constraint. - -`turn.chat_id` should become the canonical ownership pointer. `turn.specification_id` can either be removed eventually or retained as a denormalized convenience with an invariant that it matches `chat.specification_id`. - -Focus fields should be deferred. A chat may eventually focus on one item, one relation, several reconciliation needs, or a graph neighborhood. That likely wants a later `chat_focus` table rather than early nullable columns on `chat`. - -### Turn semantic-state anchor - -A turn should know the semantic state that preceded it. Historical examples below say `patch`; current implementations should read this as a changeset or semantic-revision anchor. - -Proposed addition, in this document's historical vocabulary: - -```text -turn - chat_id - preceding_patch_id -``` - -Read `preceding_patch_id` as `preceding_changeset_id` if the FE-701 schema adopts changeset naming. The field points to the latest applied semantic mutation bundle known to the chat at the moment the turn was created. This gives Brunch a durable historical anchor for reviving old chat threads. - -Example: - -```text -Chat C7 last had a turn after Changeset C12. -Elsewhere, C13-C18 changed the intent graph. -The user returns to C7. -The new turn can inject context: - "Since the last turn in this chat, these semantic changes happened elsewhere..." -``` - -This is especially important once multiple chats can mutate one specification. Without a semantic-state anchor, a dormant side chat can accidentally continue from an obsolete semantic worldview. - -If the changeset ledger is deferred, this field should also be deferred unless Brunch introduces a lightweight semantic revision/checkpoint first. Avoid adding a dangling nullable semantic-history pointer before there is a real changeset or revision concept to point at. - -### Patch *(historical name; now changeset)* - -A `patch` in this document means what current docs call a `changeset`: a semantic mutation set against the intent graph. - -It is not a workflow event and should not answer questions like "what phase is the user in?" It answers questions like: - -- what changed? -- why did it change? -- what produced the change? -- what previous semantic state did it replace? -- what downstream graph truth may now be stale? - -Proposed table, in historical naming: - -```text -patch # current name: changeset - id - specification_id - provenance_json - initiator_chat_id - initiator_turn_id - status - summary - created_at - applied_at - superseded_at -``` - -Suggested enums: - -```text -status: - proposed - applied - superseded - reverted -``` - -Provenance may want to be a discriminated JSON value rather than only an enum plus nullable foreign keys: - -```typescript -type ChangesetProvenance = // historical draft name: PatchProvenance - | { kind: 'turn'; turn_id: number; chat_id: number; capture_kind?: 'observer_capture' | 'review_acceptance' } - | { kind: 'user_direct_edit'; chat_id?: number; actor_id?: string } - | { kind: 'reconciliation_acceptance'; chat_id?: number; review_set_id?: number } - | { kind: 'verifier_result'; verifier_run_id: string } - | { kind: 'import'; source: string } - | { kind: 'migration'; migration_id: string }; -``` - -This keeps provenance extensible without adding nullable columns for every initiator shape. The relational columns `initiator_chat_id` and `initiator_turn_id` may still be useful as indexed convenience fields, but they should mirror `provenance_json`, not become a second provenance truth. - -`observer_capture` is usually initiated by a chat turn, but changeset provenance should not collapse to "chat turn." A turn can initiate a changeset; it is not the changeset. - -### Patch vs Change Naming *(resolved)* - -The proposed model has two levels: - -```text -semantic mutation set: - one user/agent/verifier action as an atomic unit - -atomic mutation: - one add/update/link/unlink/retire operation inside that unit -``` - -The naming choice was still open when this document was written: - -```text -Option A: - patch - patch_change - -Option B: - changeset - change -``` - -That choice is now resolved in favor of `changeset` / `change` because it avoids overloading "patch" with source-control connotations and because "change" naturally names the atomic unit. Under that naming: - -```text -changeset: - id, specification_id, provenance_json, status, summary, timestamps - -change: - id, changeset_id, operation, target_kind, target_id, before_json, after_json -``` - -The design question is not the word. The invariant is that Brunch needs an atomic semantic mutation set containing one or more atomic changes. The current canonical naming is `changeset` / `change`. - -### Patch Change *(historical name; now change)* - -A `patch_change` in this document means what current docs call a `change`: one operation inside a changeset. - -Proposed table, in historical naming: - -```text -patch_change # current name: change - id - patch_id # current name: changeset_id - operation - target_kind - target_id - before_json - after_json -``` - -Suggested enums: - -```text -operation: - add - update - split - merge - retire - link - unlink - verify - invalidate - -target_kind: - knowledge_item - knowledge_edge - property - example - criterion - reconciliation_need -``` - -`before_json` and `after_json` keep the first implementation practical. Later, high-volume or high-value targets can move to more normalized shape if needed. - -### Reconciliation Need - -A `reconciliation_need` is a durable mark that existing semantic truth may require renewed judgment because something changed. - -It is intentionally separate from `knowledge_edge`. - -`knowledge_edge` says something about intent semantics: - -```text -item A depends_on item B -criterion C verifies requirement R -decision D constrains requirement R -``` - -`reconciliation_need` says something about process debt: - -```text -item B changed, so item A may need review -changeset C changed an older premise, so later descendants may need coherence review -verifier V invalidated criterion C, so requirement R may need review -``` - -Proposed table: - -```text -reconciliation_need - id - specification_id - source_item_id - target_item_id - kind - status - reason - caused_by_turn_id - caused_by_patch_id # historical placeholder; current concept: caused_by_changeset_id - created_at - resolved_at -``` - -Suggested enums: - -```text -kind: - supersedes - needs_confirmation - -status: - open - resolved -``` - -This deliberately keeps phase one smaller than the fully expressive model. The first table should represent one directed process obligation from a changed source item to an affected target item, dedupe simultaneously open needs by `(source_item_id, target_item_id, kind)`, and carry enough nullable provenance to be changeset-compatible later. - -Future extensions can add: - -```text -basis / strength -source_patch_id # current concept: source_changeset_id -affected_relation_from_item_id -affected_relation_to_item_id -affected_relation -resolved_by_patch_id # current concept: resolved_by_changeset_id -structured reason payload -``` - -The `affected_relation_*` fields avoid requiring a separate `knowledge_edge.id` migration before this work can start. If `knowledge_edge` later receives a surrogate `id`, `reconciliation_need` can switch to `affected_edge_id`. - -`resolved_at` exists in phase one because no-op dismissal and non-changeset resolution are useful before the changeset ledger exists. Once changeset-backed resolution is available, the timestamp may remain denormalized convenience rather than the only resolution source of truth. - -## Reconciliation Bases - -### Semantic Dependency - -Semantic dependency is graph-based. - -Example: - -```text -Assumption A12 changes. -Relation traversal finds Requirement R4, Decision D7, Invariant I3, and Criterion C9. -Reconciliation needs are created for those affected items. -``` - -The warrant is an existing semantic relation such as `depends_on`, `derived_from`, `constrains`, `verifies`, or `refines`. - -These needs should usually be strong: - -```text -strength = needs_reconciliation -``` - -### Historical Descendance - -Historical descendance is chronology-based. - -Example: - -```text -The user directly edits Knowledge Item K4. -K4 was last updated by Changeset C12. -Later changesets C13-C31 created or updated nearby items from a context that may no longer hold. -Those later descendants receive soft reconciliation needs. -``` - -The warrant is not a known semantic dependency. It is a coherence suspicion caused by editing an older premise after later work already built on the previous state. - -These needs should usually be soft: - -```text -strength = may_need_reconciliation -``` - -Historical descendance should be grouped carefully in the UI. It can get noisy if Brunch turns every later item into an urgent individual task. - -### Verification Dependency - -Verification dependency is evidence-based. - -Example: - -```text -A verifier result invalidates Criterion C5. -Requirement R2 is linked to C5 through verifies. -R2 receives a reconciliation need because its evidence no longer holds. -``` - -This may be strong or soft depending on whether the invalidated artifact was the only witness for the affected claim. - -## Reconciliation Flow - -Reconciliation should enter the product as an agent-managed review process. - -The user experience should resemble the existing review-set flow: - -```text -agent attempts reconciliation - -> if changes are low-conflict, prepare proposed changes - -> if semantic conflict or contradiction is detected, ask the user to resolve it - -> present a reviewable set of reconciliation changes - -> user accepts or comments / requests changes - -> agent revises and presents the set again - -> accepted changes are applied as a changeset -``` - -The important difference from ordinary review sets is the agent's first move. Reconciliation should not immediately push every stale item to the user. The agent should attempt to repair, dismiss, or consolidate needs itself when the graph context is sufficient. - -Human review is required when reconciliation crosses a semantic decision boundary: - -- two accepted claims now contradict each other -- an upstream edit invalidates a downstream commitment rather than merely requiring wording updates -- multiple plausible repairs imply different product intent -- a requirement, criterion, invariant, or example would need to be weakened -- the agent cannot distinguish "update this item" from "retire this branch of intent" - -In those cases, the reconciliation turn should ask for the smallest disambiguating judgment needed, then return to the review-set loop. - -Proposed flow: - -```text -1. A semantic change is applied or proposed. -2. Deterministic traversal creates reconciliation needs. -3. Brunch collects unresolved reconciliation needs. -4. Brunch groups needs by affected target. -5. Brunch sorts needs within each target by source, basis, and strength. -6. Brunch sorts affected targets topologically so upstream repairs happen before downstream repairs. -7. Brunch groups the ordered targets into a reconciliation review set. -8. An agent proposes one of: - - no change needed - - update affected item - - retire affected item - - split affected item - - add clarifying edge or example - - ask the user a disambiguating question -9. The user accepts or requests changes. -10. Accepted reconciliation emits a new changeset. -11. The accepted changeset resolves, dismisses, or supersedes the needs. -``` - -This mirrors review-set ergonomics without pretending reconciliation is the same as requirements or criteria review. - -The loop should support revision: - -```text -reconciliation review set v1 - -> user requests changes with comments - -> agent creates revised review set v2 - -> user accepts - -> accepted reconciliation changeset is applied -``` - -Rejected or superseded reconciliation proposals should remain explainable provenance, but only accepted reconciliation should mutate the intent graph. - -### Target Ordering - -Reconciliation should operate on targets, not individual needs. A target can be a knowledge item, a knowledge relation, or eventually another semantic record such as an example or property. - -The target planner should: - -```text -collect unresolved needs -group by affected target -sort needs within target by: - 1. strength - 2. basis - 3. source item / source changeset - 4. creation time -build an affected-target graph from semantic relations -collapse cycles into strongly connected components -topologically sort components from upstream cause toward downstream dependents -process each component as one reconciliation unit -``` - -Direction matters. If `Requirement R` depends on `Assumption A`, and `A` changes, reconciliation should process `A`'s direct dependents before items that depend on those dependents. In other words, work down the dependency tree from changed source toward derived consequences. - -Cycles should not block reconciliation. They should be collapsed into a single unit and presented as a coupled coherence problem. - -If an accepted reconciliation changeset changes an upstream target, downstream needs may become superseded or may need to be regenerated from the new changeset. The reconciliation loop should therefore treat topological ordering as a work plan, not as a guarantee that one pass resolves every downstream target. - -## Can This Be Split Into Two Phases? - -Yes, with one caveat: phase one should make `reconciliation_need` future-compatible with changesets even if the `changeset` table does not exist yet. - -The split is plausible because `chat` and `reconciliation_need` each relieve a current architectural pressure independently: - -- `chat` creates the missing conversation container below `specification` -- `reconciliation_need` creates a product-visible place for staleness and coherence work -- `changeset` later upgrades provenance from turn-centered or event-centered records into a true semantic mutation ledger - -The caveat is that historical descendance is only approximate before changesets exist. Brunch can detect graph-based semantic dependency in phase one. It cannot precisely answer "which later semantic mutations descend from this older state?" until changeset history exists. - -## Phase 1: Multi-Chat Substrate and Reconciliation Need - -Goal: - -```text -Allow multiple chats per specification and introduce durable reconciliation needs without requiring the full changeset ledger. -``` - -Schema work: - -- follow [Multi-Chat Substrate](./MULTI_CHAT.md) for the concrete migration sequence -- add `chat` -- backfill one interview chat per existing specification -- add nullable `turn.chat_id` -- backfill all existing turns to the interview chat for their specification -- add `specification.primary_chat_id` -- mirror `specification.active_turn_id` into `chat.active_turn_id` -- update application writes so new turns populate both `specification_id` and `chat_id` -- add minimal `reconciliation_need` - -Compatibility rules: - -- keep `turn.specification_id` during phase one to reduce blast radius -- enforce in application code that `turn.specification_id === chat.specification_id` -- keep `specification.active_turn_id` during phase one -- scope `parent_turn_id` to the same chat in application logic -- create reconciliation needs from semantic dependency traversal first -- defer dropping legacy pointers until read/write paths are stable through chat ownership - -Phase-one reconciliation causes: - -```text -caused_by_turn_id = the turn whose observer capture or review action caused the need -caused_by_patch_id = null # historical placeholder for future changeset-backed provenance -``` - -`caused_by_kind` is intentionally omitted in the concrete phase-one schema while changesets do not exist: `caused_by_turn_id` names turn-caused needs, and the historical `caused_by_patch_id` placeholder should be read as future changeset-backed provenance. - -Phase-one limitations: - -- no exact before / after semantic diff -- no exact changeset chronology -- no reliable historical descendance beyond turn-linked provenance heuristics -- reconciliation can identify affected items, but cannot yet provide a full mutation audit - -This is acceptable if phase one frames reconciliation as "needs review" rather than as a complete semantic ledger. - -Phase-one implementation slices: - -1. Add schema and migration for `chat`. -2. Backfill interview chats and wire `turn.chat_id` on reads and writes. -3. Add invariants/tests for chat-scoped turn ancestry. -4. Add `reconciliation_need` schema and shared types. -5. Add deterministic helper to create needs from changed item plus `knowledge_edge` traversal. -6. Surface a minimal reconciliation queue in data loaders or development fixtures. - -## Phase 2: Changeset Ledger *(formerly Patch Ledger)* - -Goal: - -```text -Make semantic mutations first-class and use changesets as the source of reconciliation cause, audit, and historical descendance. -``` - -Schema work, translated to current vocabulary: - -- add `changeset` -- add `change` -- add changeset-backed cause/resolution foreign keys if they were not enforced in phase one -- optionally add `knowledge_item.last_changeset_id` -- optionally add `knowledge_edge.last_changeset_id` or give edges surrogate ids - -Application work: - -- route observer capture through changeset creation -- route accepted review outputs through changeset creation -- route direct user edits through changeset creation -- route reconciliation acceptance through changeset creation -- derive `turn_knowledge_item` as provenance compatibility or keep it as a secondary projection -- use changeset chronology for historical descendance - -Changeset application invariant: - -```text -Every semantic change to knowledge graph truth is represented by exactly one applied change inside one applied changeset. -``` - -That invariant should eventually replace "every knowledge item traces to a turn" as the semantic-history rule. - -Changeset history should make revision counts and previous values straightforward: - -```text -revision count for item K: - count applied change rows where target_kind = knowledge_item and target_id = K - -change history for item K: - applied change rows for K ordered by changeset.applied_at, including before_json and after_json -``` - -The same should hold for knowledge relations. That creates an important schema pressure: `knowledge_edge` needs stable identity if edge revision history is first-class. A composite key can identify the current relation, but it is awkward for history when a relation's source, target, or type changes. Before changeset history becomes authoritative for edges, Brunch should either: - -- add a surrogate `knowledge_edge.id` -- or replace `knowledge_edge` with a stable relation record table - -Until then, relation reconciliation can target composite relation coordinates, but relation revision history will be less clean than item revision history. - -## Migration Strategy - -### Existing Turns - -Backfill: - -```text -for each specification: - create chat(kind = primary, title = "Primary interview") - assign all existing turns for that specification to the new chat -``` - -The existing `turn.parent_turn_id` chain remains valid if all current turns in a specification belong to the primary chat. - -### Existing Knowledge Provenance - -In phase one, keep `turn_knowledge_item` unchanged. - -In phase two, create migration changesets only if the audit value is worth the complexity. A low-risk path is: - -```text -one migration changeset per specification: - provenance_json = { kind: "migration", migration_id: "changeset-ledger-backfill" } - summary = "Backfilled existing knowledge graph before changeset ledger introduction" -``` - -This avoids inventing fake historical changesets for every old observer capture. - -### Existing Knowledge Edges - -Keep composite primary keys for phase one. - -If reconciliation needs frequently target relations, phase two should consider adding a surrogate `knowledge_edge.id`. Until then, relation targets can be represented by: - -```text -affected_relation_from_item_id -affected_relation_to_item_id -affected_relation -``` - -### Existing Active Turn Pointer - -`specification.active_turn_id` can survive phase one. - -Phase one adds: - -```text -specification.primary_chat_id -chat.active_turn_id -``` - -`primary_chat_id` names the canonical interview chat; `chat.active_turn_id` mirrors the existing specification head before it becomes canonical. A separate `active_chat_id` should wait until the product has multiple active chat surfaces. Adding it too early may create unnecessary state synchronization work. - -## Invariants - -Phase one invariants: - -- every turn belongs to exactly one chat -- every chat belongs to exactly one specification -- a turn's chat belongs to the same specification as the turn -- `parent_turn_id`, when present, points to a turn in the same chat -- every reconciliation need belongs to one specification -- a reconciliation need's affected item or affected relation belongs to the same specification -- `caused_by_turn_id`, when present, points to a turn in the same specification -- the changeset-backed cause field remains null until changeset tables exist - -Phase two invariants: - -- every semantic graph mutation is represented by an applied change -- every changeset belongs to one specification -- every change belongs to one changeset -- every changeset target belongs to the same specification as the changeset -- every changeset has exactly one provenance kind -- a changeset may have chat or turn provenance, but does not require it -- hard reconciliation needs must name a concrete affected item or relation -- resolved reconciliation needs should name the changeset that resolved or dismissed them when resolution changes graph state - -## Practical Recommendation - -Do phase one first. - -The split is worthwhile because `chat` is a clear foundation for multi-conversation workspaces, and `reconciliation_need` is a useful product concept even before full semantic changeset history exists. - -But phase one should be honest about its limits: - -- it can support graph-based reconciliation well -- it can support soft, heuristic coherence review -- it cannot fully support historical descendance until changesets exist -- it should not imply a complete audit trail - -The safest phase-one framing is: - -```text -Introduce chat containers and reconciliation queues. -Keep turn-centered provenance for now. -Design reconciliation causes so changeset-backed provenance can replace turn-backed provenance later. -``` - -Then phase two becomes an upgrade of semantic provenance, not a rewrite of the reconciliation product model. - -## Open Questions - -- Should `turn.specification_id` be removed eventually, or kept as a denormalized convenience? -- Should `specification.active_turn_id` be removed as soon as `chat.active_turn_id` is stable, or kept as a temporary compatibility mirror? -- Should `chat.kind = reconciliation` own one reconciliation review set, or can one reconciliation chat cover multiple sets? -- Should direct user edits create proposed changesets first, or applied changesets with later reconciliation? -- Should `knowledge_edge` receive a surrogate `id` before reconciliation targets relations heavily? -- What is the first deterministic relation policy for creating reconciliation needs from `knowledge_edge` traversal? -- How noisy is historical descendance in realistic workspaces, and should it be grouped by changeset rather than item? diff --git a/archive/docs/design/PORTABILITY_BOUNDARIES.md b/archive/docs/design/PORTABILITY_BOUNDARIES.md deleted file mode 100644 index 065537f5b..000000000 --- a/archive/docs/design/PORTABILITY_BOUNDARIES.md +++ /dev/null @@ -1,499 +0,0 @@ -# Portability Boundaries: backend, runtime, and workspace capabilities - -> Design exploration from 2026-04-27. -> Status: **future-facing draft** — recommended direction for making Brunch portable across backend substrates and non-local agent/workspace providers. -> Canonicality: this is a focused design note for portability boundaries, not live product authority. For what is true now and what should happen next, prefer `memory/SPEC.md` and `memory/PLAN.md`. - -## Why this note exists - -Today, Brunch is intentionally local-first. - -That shows up in three product assumptions: - -- the frontend expects a local REST backend for reads and mutations -- the interview runtime expects an SSE chat endpoint for streamed assistant output -- brownfield and tool-using agent flows expect direct access to a local workspace directory and local filesystem/shell capabilities - -Those assumptions are reasonable for the current product, but they are also the main blockers to a future portable version where: - -- the backend may be local, remote, embedded, or adapter-backed -- the streaming transport may be SSE, WebSocket, RPC streaming, or an in-process iterator -- the agent may work against a local filesystem, a sandbox, a checked-out repository, or a hosted analysis substrate - -This note maps the current coupling points, evaluates several boundary shapes, and recommends the smallest deep split that makes future portability realistic without prematurely rewriting the app. - -## Current coupling map - -### 1. Frontend transport coupling - -The client currently knows concrete backend routes and fetches them directly. - -Examples: - -- `src/client/routes/__root.tsx` fetches `/api/config` -- `src/client/routes/-project-list.tsx` fetches `/api/specifications` -- `src/client/routes/specification/$id/-specification-data.ts` fetches specification bundles and entities -- `src/client/mutations/interview-mutations.ts` posts phase-intent and turn-response mutations - -This means the browser is coupled not just to a backend interface, but to a particular REST path layout. - -### 2. Frontend streaming coupling - -The interview controller creates a `DefaultChatTransport` against `/api/specifications/:id/chat`. - -That is a stronger assumption than “there is a streaming conversation backend.” It assumes: - -- HTTP request/response initiation for chat -- the AI SDK transport contract as the client boundary -- an SSE-compatible streamed response shape behind that endpoint - -The same controller also separately triggers observer capture over REST in the specification lifecycle helper. - -### 3. Server transport/runtime coupling - -`src/server/app.ts` currently mixes several layers that would ideally be separable: - -- Express route registration -- request validation and response shaping -- durable workflow mutations -- interviewer turn orchestration -- assistant streaming -- observer capture triggering -- local workspace capability injection - -This is the main portability knot. Right now, transport, orchestration, workflow, and local capabilities all meet in one place. - -### 4. Local workspace capability coupling - -Brownfield prompting and context-gathering are currently expressed in terms of a `cwd` plus local read-only tools. - -That assumption lives in `src/server/interview.ts`, where: - -- prompts mention “The workspace directory is: ${cwd}” -- exploration capability is derived from “do we have a cwd?” -- tool availability is injected via `createExplorationTools(options.cwd)` - -The tool registry in `src/server/tools/index.ts` then binds directly to local implementations such as: - -- read/write/edit file -- grep/find/list directory -- bash - -This is the key place where “agent portability” differs from “backend portability.” A portable backend can still fail to support portable agent capabilities if it only swaps storage while leaving the runtime tied to local shell and filesystem access. - -### 5. Local substrate coupling - -The launcher and runtime setup assume local project ownership and local persistence: - -- `src/server/project.ts` discovers or creates a `.brunch/` directory -- `src/server/runtime-config.ts` resolves the database path from `cwd` -- `src/server/launcher.ts` mounts the local static client and binds localhost -- `src/server/db.ts` initializes `better-sqlite3` directly - -These are valid local adapters, but they should remain adapters rather than becoming the product-level portability contract. - -## Existing seams worth preserving - -The good news is that several strong seams already exist. - -### Shared read-model contract - -`src/shared/api-types.ts` already defines the core client/server product contract: - -- `SpecificationState` -- `WorkflowState` -- `EntitiesData` -- structured turn payloads -- mutation response types - -This is the most important seam to preserve. A future portable backend should still be able to project the same specification/workflow read model even if its storage and runtime differ. - -### Read-model projection direction in SPEC - -`memory/SPEC.md` already establishes a few decisions that line up well with portability work: - -- D86: client routing is separate from center-pane rendering concerns -- D87: data ownership is partitioned by read-model domain -- D113: lifecycle side effects are specification-scoped, not route-scoped -- D121: client data ownership is moving to query-owned domains - -These decisions do not solve portability by themselves, but they do mean the product is already thinking in terms of read-model ownership versus runtime side effects. - -### Core server helper seam - -`src/server/core.ts` already acts as a partial application/service boundary around: - -- specification creation -- active-path loading -- turn preparation and finalization -- specification state projection - -It is not yet the full portability seam, but it is the right neighborhood for a durable store/read-model service. - -## Design goals - -Any portability boundary should preserve these product properties: - -1. The shared specification/workflow read model remains stable even if transport or storage changes. -2. Chat view and graph view remain projections over one durable specification truth. -3. The client keeps one specification-scoped runtime view rather than accumulating transport-specific state machines. -4. Brownfield workflows still receive contextual workspace capabilities, but those capabilities are expressed abstractly instead of assuming local filesystem access. -5. Local-first launch remains possible as an adapter, not a permanent architectural center. - -## Design A: thin transport gateway - -### Shape - -Introduce a frontend `BackendClient` that hides direct `fetch()` calls and chat transport creation, but keep the existing server structure largely intact. - -Example shape: - -```typescript -interface BrunchBackendClient { - getAppConfig(): Promise - listSpecifications(): Promise - getSpecification(specificationId: string): Promise - getEntities(specificationId: string): Promise - submitPhaseIntent(specificationId: number, request: SubmitPhaseIntentRequest): Promise - submitTurnResponse(specificationId: number, turnId: number, request: SubmitTurnResponseRequest): Promise - captureObserver(specificationId: number, turnId: number): Promise - createChatTransport(specificationId: number): ChatTransport -} -``` - -### What it hides - -- concrete `/api/...` path knowledge -- whether the browser uses `fetch`, RPC, or some embedded bridge -- chat transport creation details - -### Trade-offs - -Pros: - -- lowest-risk client extraction -- improves testability of the frontend -- removes hardcoded route layout from UI code - -Cons: - -- too shallow to solve the real portability problem -- leaves the server runtime coupled to local workspace tools and SQLite/local launch assumptions -- mostly changes “how the browser calls the server,” not “what the portable system is” - -### Verdict - -Good first step, but not sufficient as the main portability design. - -## Design B: split store, runtime, and capabilities - -### Shape - -Separate the backend into three ports: - -1. a durable specification store/read-model service -2. a session runtime that owns interview streaming and observer orchestration -3. a workspace capability provider that supplies agent-facing tools and workspace metadata - -Example shape: - -```typescript -interface SpecificationStore { - list(): Promise - create(input: CreateSpecificationRequest): Promise - readSnapshot(specificationId: number): Promise - readEntities(specificationId: number, mode: EntityProjectionMode): Promise - apply(command: SpecificationCommand): Promise -} - -interface InterviewSessionRuntime { - streamTurn(request: RuntimeTurnRequest): AsyncIterable - captureObserver(specificationId: number, turnId: number): Promise -} - -interface WorkspaceCapabilityProvider { - describeWorkspace(specificationId: number): Promise - createInterviewerTools(context: CapabilityContext): InterviewerTools - createObserverTools?(context: CapabilityContext): ObserverTools -} -``` - -### Usage sketch - -```typescript -const runtime = createInterviewSessionRuntime({ - store, - capabilityProvider, - interviewerFactory, - observerFactory, -}) - -for await (const event of runtime.streamTurn(request)) { - writer.write(event) -} -``` - -### What it hides - -- whether storage is SQLite, Postgres, in-memory, or something else -- whether streaming is delivered over SSE, WebSocket, or another transport -- whether workspace exploration hits a local repo, remote sandbox, or hosted analysis layer -- whether observer capture runs inline, deferred, or via a background job - -### Trade-offs - -Pros: - -- gives durable state, runtime orchestration, and workspace capabilities separate ownership -- aligns well with the existing distinction between read-model truth and lifecycle side effects -- keeps local-first operation as one adapter family instead of the whole architecture -- makes “portable backend” and “portable agent capabilities” separable concerns - -Cons: - -- introduces three explicit interfaces rather than one minimal facade -- requires real extraction work in `app.ts` -- pushes some naming and command-model decisions earlier than a pure transport cleanup would - -### Verdict - -This is the best fit for the current codebase. It is deep enough to hide the right complexity without collapsing everything into one giant adapter. - -## Design C: one substrate adapter - -### Shape - -Create one broad `BrunchPlatform` or `PortableWorkspace` interface that owns storage, streaming, workspace description, tool provision, and runtime operations. - -Example shape: - -```typescript -interface BrunchPlatform { - listSpecifications(): Promise - createSpecification(input: CreateSpecificationRequest): Promise - readSpecification(specificationId: number): Promise - readEntities(specificationId: number, mode: EntityProjectionMode): Promise - streamTurn(request: RuntimeTurnRequest): AsyncIterable - captureObserver(specificationId: number, turnId: number): Promise - describeWorkspace(specificationId: number): Promise -} -``` - -### What it hides - -- almost everything - -### Trade-offs - -Pros: - -- easiest interface to explain at a very high level -- could work well for a fully embedded or fully hosted product shell - -Cons: - -- too broad to be a good design seam right now -- encourages mixing durable truth, ephemeral runtime state, and capability injection behind one opaque facade -- risks turning into a god object with shallow internals instead of a deep module with crisp sub-boundaries - -### Verdict - -Too coarse for the current stage. - -## Recommended design - -Recommend **Design B: split store, runtime, and capabilities**. - -The core idea is: - -- keep the **specification read model** as the stable product contract -- isolate **session runtime orchestration** from HTTP/SSE transport -- express **workspace/agent access** as capabilities, not as “there is a local cwd” - -This design draws the portability line low enough to matter, but not so low that every implementation detail must be rethought immediately. - -## Recommended boundary map - -### 1. Frontend boundary: `BrunchBackendClient` - -The frontend should stop importing backend route knowledge directly. - -That client should own: - -- read operations for config, list, bundle, entities, export -- mutation operations for phase intent and turn response -- streaming transport construction for interview chat -- observer capture request initiation - -This does not yet make the system portable by itself, but it makes the browser depend on a coherent interface rather than a concrete REST layout. - -### 2. Durable boundary: `SpecificationStore` - -This service should own durable truth and read-model projection. - -Responsibilities: - -- specification CRUD and snapshot loading -- workflow/read-model projection -- entity projections -- command application to durable state - -It should not know about Express, SSE, browser transports, or local filesystem tools. - -### 3. Runtime boundary: `InterviewSessionRuntime` - -This service should own the turn/session behavior that `app.ts` currently coordinates: - -- prepare/resolve/finalize turn flow -- interviewer streaming -- inline or deferred observer capture -- phase summary and activity event emission -- idempotent runtime concerns around in-flight operations - -The important design point is that the runtime should emit portable events or parts, not directly write HTTP responses. - -### 4. Capability boundary: `WorkspaceCapabilityProvider` - -This is the key to making agent access portable. - -Instead of asking “do we have a `cwd`?” the runtime should ask: - -- what workspace is bound to this specification? -- what descriptive metadata can be surfaced to the agent? -- what exploration or mutation capabilities are available? -- what limitations or trust boundaries apply? - -That allows several adapters to exist later: - -- local repo + local filesystem tools -- read-only sandbox copy -- hosted repository mirror -- API-backed remote workspace analysis - -The existing local tool registry remains useful, but only as one concrete provider implementation. - -### 5. Local adapter boundary - -Keep these explicitly local and adapter-scoped: - -- `.brunch/` project discovery -- localhost binding -- local static serving -- SQLite persistence -- direct filesystem/shell tools - -These remain valid for the local product, but they should no longer define what “a Brunch backend” or “a Brunch workspace” means in general. - -## A more portable workspace concept - -The current API/config shape exposes `cwd` and `homedir` because the local app wants to render that in the chrome. - -A portable version will need a more abstract workspace descriptor. - -Example shape: - -```typescript -interface WorkspaceDescriptor { - id: string - label: string - substrate: 'local-fs' | 'sandbox' | 'remote-repo' | 'hosted-project' - displayPath?: string - capabilities: { - canReadFiles: boolean - canWriteFiles: boolean - canRunShell: boolean - canSearchCode: boolean - } -} -``` - -The important part is not the exact type. It is that the UI and prompts should depend on a descriptive workspace binding, not directly on the presence of a local cwd. - -## Incremental extraction path - -This is intentionally staged so portability can improve without a rewrite. - -### Slice 1: client transport gateway - -Introduce a `BrunchBackendClient` on the frontend and route all existing reads/mutations through it. - -Primary targets: - -- `src/client/routes/__root.tsx` -- `src/client/routes/-project-list.tsx` -- `src/client/routes/specification/$id/-specification-data.ts` -- `src/client/mutations/interview-mutations.ts` -- `src/client/routes/specification/$id/_view/-interview-controller.ts` -- `src/client/routes/specification/$id/_view/-specification-lifecycle.ts` - -This is the safest first extraction because it is mostly a dependency inversion on the client side. - -### Slice 2: extract a transport-free interview runtime - -Move the orchestration logic out of `src/server/app.ts` into a dedicated runtime service that returns portable events. - -The Express route should become: - -- validate request -- call runtime -- adapt runtime events to HTTP/SSE response transport - -### Slice 3: extract workspace capabilities from `cwd` - -Replace `cwd`-driven tool injection in `src/server/interview.ts` with a provider-driven capability model. - -Short-term, the local provider can still be implemented entirely in terms of `cwd` and the existing tools. The important move is expressing that as one provider instead of as baseline product truth. - -### Slice 4: isolate local storage/launch adapters - -Push `.brunch`, launcher, and SQLite assumptions behind explicit local adapters. - -At that point, a remote or embedded backend can implement the same store/runtime/capability interfaces without inheriting local project-discovery semantics. - -## What should stay stable - -The following should remain as stable as possible across substrate changes: - -- `SpecificationState` -- `EntitiesData` -- workflow and turn semantics -- streamed assistant/activity/observer event semantics -- graph-view and chat-view projection over shared durable truth - -In other words: portability should change *where the truth comes from and how capabilities are supplied*, not the core user-facing meaning of a specification workspace. - -## What should become swappable - -The following should become adapter-selected implementation details: - -- REST vs RPC vs embedded in-process backend calls -- SSE vs WebSocket vs another streaming mechanism -- SQLite vs another durable store -- local filesystem tools vs sandboxed repository capabilities -- localhost launcher vs hosted runtime shell - -## Open questions - -1. Should streamed chat remain the canonical browser-facing transport contract, or should the client eventually depend on a more generic async-event stream interface and let SSE be only one adapter? -2. How much of the current AI SDK `DefaultChatTransport` contract should remain part of the long-term client boundary versus being wrapped immediately? -3. For portable brownfield work, what is the minimum workspace capability set required for a good interviewer experience: file reads, search, directory listing, shell, or some higher-level analysis interface? -4. Should observer capture remain a separate runtime action, or should a more portable runtime hide that distinction entirely behind one session event stream? -5. Does a future hosted version need multi-tenant/session identity in the shared API contracts, or can that remain outside the specification/workflow model for now? - -## Recommendation summary - -If the goal is a future portable Brunch, the first real boundary should not be “replace REST.” - -It should be: - -- **stable read-model contract** -- **transport-free interview runtime** -- **capability-oriented workspace provider** -- **adapter-scoped local substrate** - -That is the smallest design that cleanly separates: - -- what Brunch *is* -- how the browser talks to it -- where the durable truth lives -- what kind of workspace the agents are allowed to touch diff --git a/archive/docs/design/README.md b/archive/docs/design/README.md deleted file mode 100644 index e2406dc6c..000000000 --- a/archive/docs/design/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Design Documents - -This directory holds exploratory and working design rationale. These files are not canonical planning state. - -Canonical project memory lives in: - -- `memory/SPEC.md` — accepted product direction, decisions, assumptions, invariants, and lexicon. -- `memory/PLAN.md` — the rolling frontier for upcoming work. -- `docs/archive/PLAN_HISTORY.md` — retired plan history. - -Use design documents for deeper argumentation, raw synthesis, alternatives, and qualifying principles that are too large for `memory/SPEC.md` or `memory/PLAN.md`. Promote conclusions into canonical memory through the `ln-spec` and `ln-plan` workflows before treating them as roadmap commitments. - -## Status language - -- `source archive / raw synthesis` — broad source material preserved for provenance; active docs may cite it, but it is not live guidance. -- `working design proposal` — a shaped proposal that may guide planning, but still needs canonical SPEC / PLAN links. -- `active synthesis` — the current cross-document concept map for a cluster; subsystem/source docs remain useful for details, but this doc owns the combined direction. -- `shipped substrate reference` — an RFC whose first implementation has landed; use it for invariants, migrations, and historical rationale, but check `memory/SPEC.md` / `memory/PLAN.md` for current status. -- `historical design pressure` — still valuable for unresolved questions or algorithms, but terminology or product shape has been superseded. -- `interim backlog` — shaped impulses that are deliberately not in the plan until their triggers fire. -- `future-facing draft` — intentionally deferred architecture map. -- `archived` — historical context only; no longer live design guidance. - -## Live index - -### Product ontology and strategy - -| Document | Role | -| --- | --- | -| `INTENT_GRAPH_SEMANTICS.md` | Product-layer ontology, edge taxonomy, relation policy, and progressive-checkability binding. Canonical design reference for FE-700. | -| `SPEC_WORKSPACE_GRAPHS.md` | Working proposal for four trace-connected graph planes — intent, oracle, design, and plan — including node kinds, semantic edges, impact index, status/provenance, and plan decomposition logic. | -| `BEHAVIORAL_KERNELS.md` | Behavioral-kernel typology, kernel cards, signal-phrase routing, and contrastive-question workflow. Canonical design reference for kernel probes. | -| `SPEC_EVOLUTION_STRATEGIES.md` | FE-705-era synthesis for chat-local strategies, scenario options, graph review, proposal turns, relation directionality, and candidate bundles. Graduated into `memory/SPEC.md` / `memory/PLAN.md`; keep as rationale. | -| `AGENT_MUTATION_SURFACE.md` | Audit of agent-originated/adjoining mutation paths and the capability/changeset boundary needed before agents write durable truth. | -| `SUBSTRATE_STRANGLER_COORDINATION.md` | Working coordination note for moving routes, capabilities, and changesets toward shared handlers while keeping frontend work stable and parallelizable. | - -### Conversational workspace runtime cluster - -Start with `CONVERSATIONAL_WORKSPACE_RUNTIME.md`. The other files in this cluster are retained source/subsystem references; do not read them as independent future roadmaps. - -| Document | Role | -| --- | --- | -| `CONVERSATIONAL_WORKSPACE_RUNTIME.md` | **Active synthesis** for the continuous workspace + unified chat + reconciliation + changeset-ledger concept. Owns the cluster supersession map and current open questions. | -| `MULTI_CHAT.md` | Shipped substrate reference for `chat`, `turn.chat_id`, `specification.primary_chat_id`, and `reconciliation_need`. Phase 2/3/4 rows are historical staging, not current sequence authority. | -| `SIDE_CHAT.md` | User-surface history and phasing for side-chat V1–V3.1, with V4 notes. Patch-list/top-bar and Pending review claims are bridge/history unless reaffirmed by the runtime synthesis. | -| `PATCH_LEDGER.md` | Historical design pressure for semantic mutation history, reconciliation bases, and target ordering. Future-facing vocabulary is `changeset` / `change`; use it for algorithms, not names. | -| `CONTINUOUS_WORKSPACE_HYBRID.md` | Workspace-shell shape exploration; owns the route-alias / workspace-controller / chart-backed-supervisor choice. | - -### Dev process and deferred impulses - -| Document | Role | -| --- | --- | -| `ln-skills/EVOLUTION.md` | Dev-layer trajectory for the `ln-*` skill family, `memory/` ontology, proposed file-backed spec registry, and possible dev/product ontology convergence. Not product SPEC. | -| `DEFERRED_RECONCILIATIONS.md` | Interim backlog for product impulses that are worthy but intentionally gated. Audit before promoting or retiring entries. | - -### Isolated / future-facing notes - -| Document | Role | -| --- | --- | -| `PORTABILITY_BOUNDARIES.md` | Future adapter/hosting/remote-workspace boundary map. | -| `GRAPH_KIND_CHIP_TOGGLE.md` | Standalone graph-view split-button chip proposal; audit against current horizon before implementation. | -| `README.md` | This index and local design-doc policy. | - -### Archived source - -| Document | Role | -| --- | --- | -| `../archive/design/INTENT_SPEC_EVOLUTION.md` | Raw synthesis / ideation source for the May 2026 intent-spec evolution work. Active docs above supersede its conclusions. | - -Schema reference artifacts are intentionally kept outside this design directory. The canonical generated DBML lives at `docs/schema.dbml` and is derived from `src/server/schema.ts`; do not add parallel `schema.dbml` or `schema.dbdiagram` copies under `docs/design/`. - -Do not create `docs/plan/` for active roadmap state. `memory/PLAN.md` remains the single source of truth for the plan; design docs may be linked from plan items as supporting rationale. diff --git a/archive/docs/design/SIDE_CHAT.md b/archive/docs/design/SIDE_CHAT.md deleted file mode 100644 index 70298594e..000000000 --- a/archive/docs/design/SIDE_CHAT.md +++ /dev/null @@ -1,449 +0,0 @@ -# Side-Chat — Design Spec - -> Output of brainstorm session 2026-04-30. Subsumes three previously-separate horizon items in `memory/PLAN.md`: graph-launched refinement (D128), trigger-popover composer, and revisit/edit mode (the archived revisit-module concept). -> -> Status: **shipped through V3.1; V4 horizon reference** — V1/V2/V3.0/V3.1 user-surface phasing has landed through FE-674. Keep this doc for shipped side-chat history, V4 notes, and UI rationale; use [CONVERSATIONAL_WORKSPACE_RUNTIME.md](./CONVERSATIONAL_WORKSPACE_RUNTIME.md) for the consolidated future runtime direction. - -## How to read this after V3.1 - -This document is now a shipped-surface and horizon-reference record, not the active runtime synthesis. - -| Claim area | Current reading | -|---|---| -| Popover-to-panel side-chat, pinned context, and brand-halo UI | Shipped/near-shipped V1–V3 surface history and UI rationale. Useful when maintaining the current side-chat panel. | -| Patch list / top-bar staging surface | Historical V1/V2 design language. The durable future is `changeset` / `change`; the long-term user surface moves into in-stream threads per `CONVERSATIONAL_WORKSPACE_RUNTIME.md`. | -| Pending review section | Shipped V3.0/V3.1 bridge surface. Long-term reconciliation absorbs into a target-grouped reconciliation thread. | -| V4a side-chat persistence | Still a plausible substrate step, but now understood as part of the unified chat/thread runtime rather than a standalone tab-strip roadmap. | -| V4b patch ledger / item versioning / architect loop | Horizon. Use current vocabulary: changeset ledger, proposal turns, graph review, and architect proposals through HITL acceptance. | - -## 1. Concept & Problem - -At the time this design was written, all interaction with Brunch's spec ran through one long interview thread: a linear back-and-forth in a single phase chat. When the user opened the structured spec view (graph view) and noticed something they wanted to discuss, edit, annotate, or refine, they had no way to act on that item *in place* — they had to navigate back to the chat and try to reintroduce the topic, often without the system understanding which item they meant. - -The side-chat added a second interaction surface: a popover-to-panel chat that opens *from* an item in the structured spec view, with selection-aware context, and that can produce durable changes to the spec through the then-current review surface called the **patch list**. The long-term runtime direction now folds this surface into a unified chat/thread stream. - -**The side-chat subsumes three horizon items:** - -- **D128 graph-launched refinement** — the disabled `chat-with` placeholder on each row in `-structured-list-view.tsx` is the seam this design activates. -- **Trigger-popover composer** (`/` commands, `@` knowledge mentions, `#` phase refs) — folded into the side-chat surface as in-chat affordances. -- **Revisit/edit mode + cascade preview** — the side-chat panel hosts the cascade preview and the secondary-thread walk, replacing the older revisit-module/modal design. - -### At a glance — user flow - -```mermaid -flowchart LR - A[User clicks chat-with
or highlights text] --> B[Side-chat
popover opens] - B --> C[Free-form chat
multi-pin if needed] - C --> D[Patches stage
in top-bar patch list] - D --> E[User reviews list
via top-bar overlay] - E --> F[Top-bar Apply] - F --> G[Durable spec
changes land] - - classDef volatile fill:#fef3c7,stroke:#d97706 - classDef staging fill:#fed7aa,stroke:#ea580c - classDef durable fill:#dbeafe,stroke:#2563eb - class B,C volatile - class D,E,F staging - class G durable -``` - -Yellow = volatile (no effect yet) · orange = staged (proposed but not committed) · blue = durable (applied to spec). The patch list lives in the persistent top-bar (`N Edits · Undo · Apply`) — visible whether or not the side-chat panel is open. - -## 2. Surface & Lifecycle - -A hybrid popover-to-panel surface anchored to items in the structured spec view. - -- **Two entry modes:** - - **Button entry** — each row's action rail exposes a `chat-with` button. Clicking opens a popover (~360px wide) anchored to the row, with the *whole item* attached as item-level pinned context. - - **Selection entry** — the user drags-selects text inside any item's content or rationale. A small floating menu appears anchored to the cursor with two affordances: `💬 Chat` and `📝 Annotate`. Both attach the *highlighted span* as span-level pinned context (excerpt + parent-item reference). `Chat` opens the popover normally; `Annotate` skips the chat and opens the inline annotate form directly with the span pre-filled. -- **Expansion.** The user can expand the popover into a full-height right-side drawer when a thread runs long. Multi-item pinning, the patch list, and the cascade preview all use the expanded panel. -- **Persistence.** The panel persists for the spec session; navigating within the spec preserves the panel and its thread. Closing the panel ends the session. -- **Multi-item pinning is the default model.** A single-item chat is the degenerate case (one pinned context card; same UI). Clicking `chat-with` or selecting text on additional rows pins them as context cards in the panel; the chat reasons across all pinned items. -- **Anchoring.** In V1, the surface is reachable only from the graph view at `/specification/$id/graph`. When the continuous workspace lands, every visible knowledge item across phase sections inherits both entry modes. -- **Single thread per spec session in V1.** The panel hosts one running thread, anchored to the spec. Multiple parallel threads (Figma's `New chat` / `Old chat` tab strip) are deferred to V4 once the patch / event-stream model lets old threads persist durably. V1 may render the tab strip with `New chat` active and `Old chat` shown but disabled — preserves the visual language without the durable-state requirement. -- **Top-bar patch summary.** The persistent app top-bar surfaces the running patch counter — `N Edits` · `Undo` · `Apply` — whenever staged patches exist. This is the canonical patch-list surface (§4); the side-chat panel surfaces patches inline as they're staged but defers to the top-bar for the full list and apply action. - -### Entry paths converging on the patch list - -```mermaid -flowchart TD - E1[Click chat-with
on item row] --> P[Side-chat
popover opens] - E2[Highlight text
inside an item] --> M[Floating selection menu
💬 Chat · 📝 Annotate] - M -->|Chat| P - M -->|Annotate| AF[Inline annotate form
span pre-filled] - P --> CTX{Pinned context shape} - CTX -->|Item-level| FF[Free-form chat
over whole item] - CTX -->|Span-level| FS[Free-form chat
biased to highlighted span] - FF --> PL[Patch list] - FS --> PL - AF --> PL - PL --> A[Bulk apply] - - classDef entry fill:#e0e7ff,stroke:#4f46e5 - classDef volatile fill:#fef3c7,stroke:#d97706 - classDef staging fill:#fed7aa,stroke:#ea580c - classDef durable fill:#dbeafe,stroke:#2563eb - class E1,E2 entry - class P,M,FF,FS,CTX volatile - class PL,AF staging - class A durable -``` - -Two entry surfaces, three internal paths, one staging area, one apply step. The `Annotate` shortcut from selection-entry skips the chat phase entirely — the user can leave a span-anchored note in two clicks. - -## 3. User-Facing Intents & Internal Taxonomy - -The side-chat has three orthogonal dimensions: - -| Dimension | Values | Visible to user? | -|---|---|---| -| **User-facing mode** | Explore · Edit · Annotate | Yes — three buttons in the chat surface | -| **Patch kind** | `edit` · `edge` · `drill-down` · `annotate` | Yes — each staged patch shows its kind in the patch list | -| **Impact tier** *(for `edit` patches only)* | `none` · `soft` · `hard` | No — system-internal routing | - -**Mode → kind mapping:** - -- **Explore** never stages a patch. Pure conversation. -- **Edit** stages patches with kind `edit`, `edge`, or `drill-down`, depending on what the chat surfaces. The chat (model-driven) selects the kind based on the conversation: a wording change → `edit`, a graph relationship proposal → `edge`, a "deepen this area" intent → `drill-down`. -- **Annotate** stages a patch with kind `annotate` directly. - -**Edit's impact tier is system-decided.** When an `edit` patch applies, the system inspects the anchor item's graph topology and picks the durability path (none / soft / hard — described in §5). The user never has to know the difference between a Refine, a Soft edit, and a Hard edit — those are tier-routing outcomes within the same `edit` patch kind. - -This 3-mode collapse was driven by Lu's note: users mentally bucket *exploratory / edit / annotate*, not the 4-class taxonomy that the backend uses. - -### Mode → Kind → Path - -```mermaid -flowchart LR - subgraph M[User-facing modes] - M1[Explore] - M2[Edit] - M3[Annotate] - end - subgraph K[Patch kinds] - K1[no patch] - K2[edit] - K3[edge] - K4[drill-down] - K5[annotate] - end - subgraph P[Durability paths] - P1[Volatile
no effect] - P2[Refine / Soft
recompute / Hard
cascade] - P3[Validate edge +
persist] - P4[Detail-focus intent
steers next frontier turn] - P5[Per-item note
via comment store] - end - - M1 --> K1 --> P1 - M2 --> K2 --> P2 - M2 --> K3 --> P3 - M2 --> K4 --> P4 - M3 --> K5 --> P5 - - classDef mode fill:#e0e7ff,stroke:#4f46e5 - classDef kind fill:#fef3c7,stroke:#d97706 - classDef path fill:#dbeafe,stroke:#2563eb - class M1,M2,M3 mode - class K1,K2,K3,K4,K5 kind - class P1,P2,P3,P4,P5 path -``` - -Three buttons (left), four patch kinds (middle), five distinct durability outcomes (right). The user only sees the modes; the system handles the rest. - -## 4. The Patch List (Staging Surface) - -The single most consequential design choice in this revision: **promotion is staged, not single-click.** - -Instead of *click promote button → one action fires*, the user (and the chat itself) **stages multiple proposed changes** into a list. The user reviews the staged list and applies in batch. - -The patch list has **two surfaces** that share one underlying state: - -- **Top-bar summary** *(canonical)* — the persistent app top-bar shows `N Edits` · `Undo` · `Apply` whenever ≥1 patches are staged. Clicking `N Edits` opens an overlay listing all staged patches with full per-entry detail. `Apply` performs bulk-apply across all staged patches. `Undo` rewinds the most recent applied batch. Visible regardless of whether the side-chat panel is open. -- **In-panel inline surfacing** *(secondary)* — when patches are staged from inside the side-chat, they animate into a compact list near the bottom of the panel as visual acknowledgment. Per-entry actions (`Edit summary`, `Discard`) are reachable from there without opening the top-bar overlay. This is convenience UI, not source of truth — the canonical state lives in the top-bar. - -### 4.1 Patch entry shape - -Each patch in the list carries: - -| Field | Purpose | -|---|---| -| **Kind** | `edit` / `edge` / `drill-down` / `annotate` | -| **Anchor item(s)** | Reference codes (e.g. `[C1]`, `[G2]→[C2]` for edges) | -| **Selection range** *(optional)* | `{ start, end, snapshotText }` — present only when entered through the selection menu and the patch carries span context. Currently used only by `annotate` patches in V1; `edit`-kind span anchoring is deferred to the patch / event-stream model (A71). | -| **Summary** | One-line human-readable description | -| **Impact tier** | `none` / `soft` / `hard` — drives soft-vs-hard edit routing (§5) | -| **Detail** | Expandable: full payload, affected items, prompt context | -| **Per-entry actions** | `Apply` · `Edit summary` · `Discard` | - -**Patch granularity stays item-level in V1.** Even when the chat reasons over a span-level pinned context, the resulting patches anchor to the parent item. Span context is a *prompting hint* (the model is biased to discuss the highlighted span), not a patch granularity. The single exception is `annotate`, which can carry a `selectionRange` so the annotation visibly points to the highlighted phrase (§6.4). - -### 4.2 How patches enter the list - -- **Chat-proposed.** During a chat exchange, when the conversation surfaces a concrete change, the chat (model-driven) proposes a patch into the list with a brief acknowledgment (e.g. "Staged: edit C1 to widen 'family' to 'household'"). -- **User-explicit.** The user can click a "Propose patch" affordance in the chat input area to manually add a patch (kind + anchor + summary) without going through the chat dialogue. -- **Drag from chat.** The user can drag a chat reply that contains a proposal into the patch list to stage it. - -### 4.3 Bulk apply - -The top-bar `Apply` button performs **bulk-apply** across all staged patches in dependency order. Patches with conflicting anchors (two edits on the same item) prompt for resolution before apply. The button is the single canonical commit affordance — there is no per-entry apply in V1; users either apply everything staged or discard what they don't want first. - -### 4.4 Why this matters - -Historical V1/V2 reading: the patch list was designed as **the unifying review surface for all spec mutations** so later architect-loop proposals would have somewhere to deposit. Current target reading: the review unit is still HITL and batchable, but future durable semantics should be expressed as proposal turns and accepted changesets inside the unified runtime rather than a separate long-lived patch-list surface. - -## 5. Edit Patch Routing - -```mermaid -flowchart TD - A[Apply edit patch] --> B{Anchor item's
phase status?} - B -->|Open| C[Refine path

Create successor turn
with revision card
Interviewer reviews] - B -->|Closed| D{Impact tier?} - D -->|None
0 downstream| E[Apply directly

Single-item update
No cascade] - D -->|Soft
1-2 downstream
not in review set| F[Soft recompute

Apply + recompute
affected items inline] - D -->|Hard
high impact OR
in active review set| G[Cascade preview

Batch secondary-thread
resolution] - - classDef decision fill:#fef3c7,stroke:#d97706 - classDef path fill:#dbeafe,stroke:#2563eb - class B,D decision - class C,E,F,G path -``` - -When a patch with kind `edit` is applied, the system routes by **two questions in order**: - -1. **Is the anchor item's phase OPEN or CLOSED?** - - **Open** (anchor item is in the current frontier-bearing phase) → **Refine path** (§6.2): create a same-phase successor turn with a revision card. The interviewer reviews and accepts as part of normal turn flow. If downstream items exist, the cascade preview from §5.3 still applies before the successor turn lands. - - **Closed** (anchor item is in a phase whose lifecycle is closed) → continue to (2). -2. **What is the impact tier of the retroactive edit?** *(§5.1)* - -### 5.1 Impact tiers (retroactive edits, anchor item in closed phase) - -| Tier | Trigger | Path | -|---|---|---| -| **None** | `affectedCount === 0` (item is a graph leaf with no downstream edges) | Apply directly. Single-item content update; brief inline confirmation card in the panel: "Updated `[X]`." | -| **Soft** | `1 ≤ affectedCount ≤ 2` AND no anchor or affected item is in an active review set *(active = generated and not yet accepted)* | Apply directly with affected-item context. Patch lands directly; brief inline confirmation lists the affected items: "Updated `[X]`; `[Y]`, `[Z]` may need a refresh." No cascade preview or durable `reconciliation_need` rows. | -| **Hard** | High downstream count, OR any anchor or affected item is in an active review set | **Cascade preview** backed by `reconciliation_need` rows → batch-resolution mode in the side-chat panel (§5.3). The archived [REVISIT_MODULE](../archive/design/REVISIT_MODULE.md) walk is superseded. | - -### 5.2 Confidence model — V1 - -V1 ships with **mechanical-only** routing: `affectedCount` and review-set membership are deterministic and trivially computable from the existing graph state. No semantic-shift detection in V1. Tuning the count thresholds (currently `0` for None, `1–2` for Soft, `3+` for Hard) is deferred until we have user-flow data on whether soft-edit feels too aggressive or too cautious. - -### 5.3 Hard edit — cascade through the reconciliation queue - -Hard-edit cascade is no longer a one-shot REVISIT walk. In this stack, the downstack multi-chat substrate (FE-697) provides a durable `reconciliation_need` queue: directed item-to-item rows with `kind ∈ { supersedes, needs_confirmation }`, `status ∈ { open, resolved }`, partial unique index on open rows, and `caused_by_turn_id` provenance. V3 reads from that queue after FE-697 lands. - -**On hard-impact apply:** - -1. The source content change lands as in V2 None/Soft. -2. The server enumerates `knowledge_edge` rows incident on the changed item under typed relation policy (`depends_on`, `derived_from`, `constrains`, `refines`, `verifies`) — Path 1 from `docs/design/MULTI_CHAT.md` §5.1. -3. For each affected pair, an open `reconciliation_need` row opens with the appropriate kind. Re-firing on an already-open `(source, target, kind)` is a no-op (partial unique index). -4. Open needs surface in the patch list overlay as a `Pending review` section. -5. The user resolves each need through a single **Resolve** action per row. The action transitions `(open, resolved_at = null) → (resolved, resolved_at = now)` idempotently and does **not** mutate any target item. Users wanting to edit a cascade target use the existing structured-list inline-edit affordance separately (which itself may open further needs — re-entrant cascade, intentional). The richer three-action design originally sketched here — `accept-on-target` / `edit-target` / `dismiss` — is V3.1 work, surfaced through the reconciliation agent's grouped resolutions below. - -**V3.0 grouping is mechanical, not agent-classified.** Needs are grouped in the overlay by `kind` (`supersedes` first, then `needs_confirmation`) and within each kind by relation type. There is no auto-confirm / auto-edit / substantive ML classification in V3.0. - -**V3.1 — agent-grouped resolution (shipped, FE-674 PR #124).** The reconciliation classifier (server `POST /api/specifications/:id/reconciliation-needs/run-agent`) walks every open need through the lifecycle `null → queued → classifying → classified | failed` (I114) and persists exactly one of: - -- **`auto-confirm`** — review-only affected items where no content change is implied by the source change. One-click Confirm resolves without mutating the target. Bulk "Confirm all (N)" iterates serially over auto-confirm rows. -- **`auto-edit`** — items where the change is a mechanical text replacement. `agent_proposal` carries the suggested new content; the user previews via the `` (View) and then Apply (edit + resolve) or Skip (resolve only). Bulk "Apply all suggested (N)" iterates serially. -- **`substantive`** — items where user judgment is required. "Open side-chat" hands `useSideChat().openFor` the target's kind + reference code + content; the conversation walks the substantive case (currently ephemeral until V4a persistence ships). - -`failed` rows are recoverable via per-row Re-run (`POST /api/specifications/:id/reconciliation-needs/:needId/reset-agent`), which resets `agent_status` to null and re-dispatches the classifier on that row. - -**Canonical route shape:** classifier traffic is always spec-scoped under `/api/specifications/:id/reconciliation-needs/…` (the two routes above). There is no bare root-level `POST /reconciliation-needs/run-agent` on this server—spell the full path in docs, tests, and runbooks so operators are not sent to the wrong URL. - -A typical 5-item cascade in V3.1 collapses from ~5 sequential resolutions to 2 group decisions + 1 substantive walk; V3.0 surfaced all 5 mechanically. A88 outer-loop walkthrough is the qualitative validation that grouping helps legibility vs the flat V3.0 list. - -## 6. Class-Specific Durability Mechanics - -For reference. Each is invoked by the patch-list bulk-apply. - -### 6.1 Class 1 — Explore - -Volatile chat thread. Nothing leaves the panel. Discarding the panel ends the thread. - -### 6.2 Class 2 — promote-to-turn intents - -| Intent | Mechanism | -|---|---| -| **Refine** | Patch with kind `edit` whose anchor item is in the **open** phase (per §5 routing). Creates a same-phase successor turn with a revision card stacked on a question card. Reuses Requirement 25's revision pattern. If the anchor has downstream items, the cascade preview from §5.3 runs before the successor turn lands. | -| **Drill-down** | Patch with kind `drill-down`. Emits a `detail-focus` intent attached to the anchor's reference code. Steers the next frontier turn in the relevant open phase. Reuses D127's progressive-detail seam. | -| **Propose-edge** | Patch with kind `edge`, anchor is a pair of items. Validated through D125's typed relation-policy registry. If valid, persists; if not, returns policy feedback in the patch detail. | - -### 6.3 Class 3 — promote-to-edit intents - -Routed via §5. Soft and Hard edits both produce durable item content changes; the difference is whether the user sees cascade preview before commit. - -### 6.4 Class 4 — Annotate - -Patch with kind `annotate`. Persisted as an `item_annotation` row keyed by `(specificationId, itemKind, itemId, authorTurnIdOrNull)`. **Annotations and review-set per-item comments share one comment store** — they're the same row type, distinguished only by an `origin` field (`annotation | review-comment`). One annotation IS one comment. - -**Span-anchored annotations.** When entered through the selection menu, the patch carries `selectionRange: { start, end, snapshotText }`. The annotation row stores the range alongside the note. `snapshotText` is the highlighted phrase at the time of save — used for fuzzy reattach if the rationale or content is later edited. - -Surfacing rules: - -- **Graph view row** — badge in the action rail (count + hover preview). Span-anchored annotations show the excerpt above the note in the hover. -- **Inline content tint** — when the row is expanded and the parent text is rendered, span-anchored annotations apply a subtle background tint to the highlighted phrase. Clicking the tint opens that annotation directly. -- **Review-set card** — when a review set is regenerated and includes the anchor item, the existing annotation surfaces as the per-item comment for that row, pre-filled but editable. The user can keep, edit, or clear it for fresh review feedback. -- **Relation-chip hover** — `RelationChipPreview` extends to show "📝 N" if the linked item has annotations; clicking the preview opens the side-chat with the annotation pinned. - -**Drift handling without versioning.** If the parent item's content is later edited and `snapshotText` no longer matches its original position, fuzzy reattach attempts to find the new location. On low-confidence match or no match, the annotation degrades to **item-level** with a "🔗 originally referenced text that has since changed" indicator surfaced inline; the user can re-confirm by clicking through to the annotation, optionally re-anchoring to a new selection. The original `snapshotText` is preserved as audit. Once item versioning lands (A72), span anchors can attach to a specific version and never silently drift. - -## 7. Out of Scope (Acknowledged Adjacents) - -- **Architect / generator loop.** An autonomous agent that iterates over the graph and proposes changes for HITL review. Symmetric to the side-chat in *what it does* but different in *who drives*. Future Horizon item in `memory/PLAN.md`. Will deposit into the same patch list once the patch / event-stream data model lands. -- **Explicit chat-level branching UI.** The `spec → chat → turns` future data model implicitly allows multiple chats per spec, but no "branch this thread" affordance ships. D80's no-turn-branching invariant stays in force. -- **Cross-spec annotation sharing.** Out of scope per the existing constraint of no collaborative editing. -- **Slash commands / `@` mentions / `#` phase refs.** The trigger-popover composer's command syntax is folded conceptually but not implemented in V1; the panel uses click-to-pin and free-form chat as the input model. Slash commands ship as a later enhancement. - -## 8. Dependency Assumptions - -The side-chat's substrate dependencies have shifted as the multi-chat work landed. Two assumptions are unchanged; one is partly satisfied. - -### A71 *(partly satisfied)*: chat substrate plus semantic mutation ledger - -The original framing — `spec → chat → turns` with diff patches as the persistence primitive — is split. In this stack, the `spec → chat → turns` half is supplied by downstack FE-697: a `chat` table, nullable `turn.chat_id`, `specification.primary_chat_id`, mirrored `chat.active_turn_id`, and a `reconciliation_need` queue with a future semantic-mutation cause placeholder. The changeset ledger half remains horizon work tracked historically in `docs/design/PATCH_LEDGER.md` and currently in `memory/PLAN.md` as the semantic changeset ledger. - -**Implication for V3.** The cascade preview reads `reconciliation_need` rows directly (see §5.3, §13). Side-chat threads themselves stay in-memory through V3 — durable side-chat persistence is MULTI_CHAT.md Phase 2 / V4 and is **not** a V3 prerequisite. - -**Implication if the changeset ledger lands later:** reconciliation needs gain changeset-backed cause/resolution provenance; resolutions write changesets; the in-memory patch list either retires into proposal-turn state or translates through a compatibility layer. No user-facing change to shipped V3 surfaces is required. - -### A72: knowledge-item versioning - -History per knowledge item, preserved through edits. Anchors annotations to specific versions, preserves audit trail on soft edits, lets revisit cascades produce new versions instead of invalidating. - -**Implication if this lands later:** annotations can survive item edits without dangling; soft edits become trivially reversible. V1 ships without versioning; annotations float over current item content and may dangle on edit (rare in V1, accepted risk). - -### A73: architect / generator loop - -Captured in §7. The side-chat is *user-driven*; the architect is *system-driven*. Historical design routed both into the patch list; current design routes architect proposals through proposal turns and accepted changesets, with graph review as the safety oracle. - -## 9. Phasing - -| Version | Ships | -|---|---| -| **V1** | Popover-to-panel surface · multi-pin · Class 1 (Explore) · Class 4 (Annotate). Patch list surface (top-bar summary + overlay) introduced but holds at most one entry (annotation only). Single thread per spec session; tab strip rendered with `Old chat` disabled placeholder. No Edit, no Drill-down, no Propose-edge. | -| **V2** | Edit (router) · Drill-down · Propose-edge in the patch list. **None** and **Soft** edit tiers apply directly. **Hard** edit defers to a placeholder "feature coming" message. Refine routes through normal turn machinery. | -| **V3.0** | Hard-edit apply opens `reconciliation_need` rows from existing graph edges (Path 1, deterministic). Cascade preview surfaces as a `Pending review` section inside the canonical patch-list overlay; **single per-row Resolve action** that idempotently transitions `open → resolved`. The V2 `deferred: true` server response and the "Hard impact — coming in V3 cascade preview" banner are removed. Acceptance Criterion #7 satisfied mechanically. No reconciliation agent. REVISIT modal stays archived. (Note: the original three-action design — `accept-on-target / edit-target / dismiss` — is collapsed to a single Resolve in V3.0 because the open→resolved transition is the same regardless of intent label; V3.1 reintroduces richer kinds via the agent.) | -| **V3.1** *(shipped, FE-674 PRs #119–#124)* | Reconciliation classifier writes `agent_status` / `agent_classification` / `agent_proposal` per row. Pending review surface renders chips, Run-agent + polling (`POST /api/specifications/:id/reconciliation-needs/run-agent`), per-row Re-run (`POST /api/specifications/:id/reconciliation-needs/:needId/reset-agent`), per-class actions, and bulk Confirm-all / Apply-all-suggested. Substantive walk lands inside the side-chat panel using pinned-context conversation. Path 2 observer expansion still horizon. | -| **V4a** *(horizon / runtime-track input)* | Side-chat client persists turns into `chat` / `turn` with `chat.kind='side_chat'`; "Old chats" tab strip activates. Current runtime synthesis may instead render side conversations as in-stream threads. | -| **V4b** *(horizon, FE-675 V4b half + FE-701)* | Changeset ledger lands. Reconciliation needs gain semantic-mutation cause/resolution provenance; item versioning anchors annotations and soft-edit audit. Architect-loop proposals use the same HITL proposal/changeset pathway rather than committing graph truth directly. | - -## 10. Verification Stance - -| Loop | Coverage | -|---|---| -| **F1 component tests** | Popover, panel, patch-list rendering, per-entry kind variants, cascade preview, secondary-thread item-walker. | -| **F2 router-integrated tests** | `chat-with` → popover → multi-pin → patch list → apply → main-flow effect. Specifically: drill-down apply → next-turn intent chip → question-card materialization steered by the intent. | -| **F3 a11y** | Popover and panel keyboard navigation, focus return on dismiss, ARIA labels for patch-list entries. | -| **F5 network-call counter** | Soft-edit auto-apply does **not** trigger cascade-preview endpoints. Hard-edit triggers exactly one cascade preview. Apply triggers exactly one bulk-apply call regardless of patch count. | -| **F6 fixture matrix** | Impact-tier routing fixtures: leaf-edit (`none`), 2-downstream-edit (`soft`), in-review-set-edit (`hard`), edge-proposal-with-policy-violation (rejection path). | -| **F7 dramaturgical walkthrough** | The three flows already documented (Edit-with-cascade, Drill-down, Annotate) plus one new flow: multi-patch staging → bulk apply across mixed kinds. | - -## 11. UI Language - -This section codifies the design tokens and visual conventions adopted from the HASH product surface (`figma.com/design/nTw9n0blCJm1j9t22Jo72d` — node `969:386`). Brunch's existing CSS-variable token system already covers most of the Figma palette; entries below note where the Figma maps onto existing brunch tokens versus where a new pattern is introduced. Elements present in the Figma reference but not relevant to the side-chat (Safari chrome, brunch's existing phase navigation sidebar treatment, page-title gradient text, skeleton placeholders) are intentionally not adopted here. - -### 11.1 Typography - -- **Family:** Inter (400 regular, 500 medium). Already in use across brunch. -- **Sizes:** - - 14px — primary UI / body / button labels - - 13px — secondary panel rows - - 12px — tertiary / sub-items - - 11px — small label tags (e.g. impact tag) -- **Line-height:** 1.6 for body, `leading-none` for compact buttons / chips, 16px for compact UI labels. - -### 11.2 Color tokens - -| Figma hex | Role | Brunch token | -|---|---|---| -| `#202020` | Primary ink | `text-ink` | -| `#5b5b5b` | Secondary text | `text-sub` | -| `#a6a6a6` | Hint / muted text | `text-hint` | -| `#ffffff` | Card surface | `bg-background` | -| `#fafafa` | Soft surface, panel wash | `bg-tint` | -| `#f2f2f2` | Chip fill, secondary button | `bg-wash` | -| `#e3e3e3` | Borders, rules | `border-rule` | -| `#2070e6` | Primary blue (active states, links) | existing brunch primary | -| `#3484fa → #2070e6` | Primary CTA gradient | new — only for primary buttons | -| `#e14640` | High-impact red label | new — `text-impact-high` | -| HASH brand gradient | Side-chat panel halo | new — see §11.5 | - -### 11.3 Spacing & rounded scale - -- **Rounded scale:** 4 / 6 / 8 / 12 / 16. Buttons 6, chips 4, cards 8 / 12, panels 16. -- **Padding:** chip 6–8px, button 8–12px, card 12px, panel outer 8px / inner 12px. -- **Component sizes:** icon button 24×24 or 28×28; chip height 24–28; standard button 32; card row 32. - -### 11.4 Components - -| Component | Treatment | -|---|---| -| **Primary button (Apply, Send)** | Gradient `#3484fa → #2070e6`, ring shadow `0 0 0 1px #1060d6`, multi-stop drop shadow stack, inner `0 1 1 rgba(255,255,255,0.2)` highlight, white text, 6px rounded. | -| **Secondary button (Undo, Skip, Back)** | `bg-wash` fill, no border, muted text (`#a6a6a6`), 6px rounded. | -| **Soft chip** | `bg-[rgba(0,0,0,0.03)]`, 4px rounded, 6px padding, 14px regular text, optional `×` dismiss on the right. Used for pinned context cards. | -| **Card (white surface)** | `bg-background`, `border border-rule`, 12px rounded, 16px padding. Optional 4-stop drop shadow for elevated state. | -| **Activity card** | Animated gradient text on the header row + indented step list with a 1px vertical rule on the left edge. Used for the drill-down "pending intent" indicator and the live generation indicator. | -| **Tab strip** | Two-tab row with active tab as a white card (shadow ring) and inactive tab as a transparent fill (`bg-[rgba(0,0,0,0.04)]`). | - -### 11.5 Side-chat brand surface - -The side-chat panel uses a **frosted-glass + brand-halo** treatment that visually separates it from the main interview surface: - -- **Backdrop:** `backdrop-blur-12px` with `bg-white/70`. -- **Border:** 1.5px solid `#5424ff` at 55% opacity. -- **Halo:** outer blurred gradient ring using the HASH brand colors (purple `#5424ff` → orange `#fdb975` → pink `#fe5dd3` → magenta `#ff00ae`), 25% opacity, 20px blur. -- **Rounded:** 16px outer, 12px inner cards. -- **Anchor:** top-right of the spec view, ~588px wide when expanded to panel; ~360px when in popover state. - -This brand-halo is used **only on the side-chat panel** in V1. The main interview surface, the graph view, and the phase navigation sidebar retain their flat / surface-only treatment. The brand halo signals "this is a generative AI surface" — distinct from the durable-spec surfaces around it. - -### 11.6 Top-bar patch summary - -The top-bar patch summary (§4) sits in the persistent app top-bar above the workspace stream and graph view. When zero patches are staged, it shows nothing extra. When ≥1 patches are staged: - -- **Counter:** `N Edits` in `text-sub` Inter medium 14px, with chevron-down toggle for the overlay. -- **Secondary `Undo` button:** chip-style on `bg-wash`. -- **Primary `Apply` button:** gradient CTA per §11.4. - -Clicking the counter opens an overlay panel listing staged patches. The overlay can be opened independent of the side-chat panel — the architect loop in V4 will deposit patches that the user can review and apply without ever opening the side-chat. - -## 12. Open Questions for Implementation - -- **Confidence model heuristics.** What `affectedCount` threshold is right for soft vs hard? Probably needs A/B observation post-V2 once we have flow data. -- **Patch conflict resolution.** When two staged patches modify the same anchor, what's the resolution UX? Likely: surface conflict at apply time, ask user to pick or merge. Defer detailed design to V2. -- **Chat-runtime sharing.** Does the side-chat use the main interview's runtime (cheaper context, shared cost) or a separate scoped runtime (clean isolation)? V1 uses a separate runtime to avoid coupling; revisit if token cost becomes an issue. -- **Patch list persistence across page reload.** Does the patch list survive a browser reload, or is it session-scoped only? V1: session-scoped, in-memory. Persist when the patch / event-stream data model lands (A71). - -## 13. Substrate Alignment - -V-versions in §9 describe the *user surface*; substrate phases in `docs/design/MULTI_CHAT.md` §10 describe the *data model*. They evolve independently. Mapping at time of writing: - -| User-surface version | Substrate phase requirement | Notes | -|---|---|---| -| V1 (Explore + Annotate) | Phase 1 not required | Shipped against in-memory patch list. | -| V2 (Edit / Drill-down / Propose-edge, None+Soft tiers) | Phase 1 not required | Shipped against in-memory patch list; hard branch returns `deferred: true`. | -| V3.0 *(shipped, FE-674 PRs #115-#118)* | Phase 1 read side | Hard apply writes `reconciliation_need` rows; UI reads the queue. Per-row Resolve / Edit-target / View-source-diff. No agent. | -| V3.1 *(shipped, FE-674 PRs #119-#124)* | Phase 3 | Reconciliation classifier writes `agent_status` / `agent_classification` / `agent_proposal` per row. Pending review surface renders `` (six variants), Run-agent button with conditional 1s polling, per-row Re-run on classified/failed rows, per-class actions (`auto-confirm` → Confirm, `auto-edit` → View / Apply / Skip, `substantive` → Open side-chat via `useSideChat().openFor`), bulk Confirm-all (N) and Apply-all-suggested (N) iterating serially. **HTTP:** `POST /api/specifications/:id/reconciliation-needs/run-agent` and `POST /api/specifications/:id/reconciliation-needs/:needId/reset-agent` (§5.3). | -| V4a *(horizon / runtime-track input)* | Phase 2 | Side-chat client persists turns into `chat` / `turn` with `chat.kind='side_chat'`; "Old chats" tab strip activates in this document's original model. Current runtime synthesis may fold this into in-stream threads. | -| V4b *(horizon, FE-675 V4b half)* | Phase 4 | Changeset ledger (FE-701); item versioning; branched exploration; architect loop. | - -**Decisions and assumptions that govern V3.0:** - -- Decision **D135** — semantic mutation history splits from turn history; revisit/cascade product capability stays live; `revisit_session` is no longer the persistence foundation. -- Decision **D137** — knowledge edges carry intent semantics; `reconciliation_need` is process debt. The two never merge. -- Decision **D138** — multi-chat substrate is the first concrete persistence slice; the queue carries `caused_by_turn_id` immediately and `caused_by_patch_id` later. -- Decision **D139** *(new in V3.0)* — hard-impact edit cascade reads from the `reconciliation_need` queue; `deferred: true` is removed from the apply contract. -- Assumption **A71** — partly satisfied (see §8). -- Assumption **A84** *(new in V3.0)* — Path 1 deterministic enumeration over existing `knowledge_edge` rows yields a useful cascade preview without requiring the reconciliation agent in V3.0. -- Invariant **I112** *(new in V3.0)* — hard-impact apply opens at least one `reconciliation_need` per existing dependency edge incident on the changed item, and the apply contract no longer returns `deferred: true`. - ---- - -## Traceability - -- **Replaces** PLAN.md horizon items: graph-launched refinement (under D128), trigger-popover composer, revisit / edit mode + cascade preview (the older revisit-module/modal concept is subsumed by this design). -- **Reuses** D125 (typed relation policy), D127 (progressive-detail seam), D128 (graph view actionable workspace mode), Requirement 25 (revision card pattern). -- **Adds** future assumptions A71 (patch/event-stream model), A72 (item versioning), A73 (architect loop). -- **Bounded by** D80 (no turn-tree branching), D89 (card-owned input), D113 (no second durable workflow model), D66 (user authorizes). diff --git a/archive/docs/design/SPEC_EVOLUTION_STRATEGIES.md b/archive/docs/design/SPEC_EVOLUTION_STRATEGIES.md deleted file mode 100644 index 1045451ed..000000000 --- a/archive/docs/design/SPEC_EVOLUTION_STRATEGIES.md +++ /dev/null @@ -1,244 +0,0 @@ -# Spec Evolution Strategies - -> Status: **design RFC — graduated into `memory/SPEC.md` / `memory/PLAN.md`**. -> Date: 2026-05-12. -> Scope: chat-local strategies for advancing a Brunch specification's intent graph from vague user intent toward phase-mature, reviewable semantic truth. -> -> Related docs: [`AGENT_MUTATION_SURFACE.md`](./AGENT_MUTATION_SURFACE.md), [`BEHAVIORAL_KERNELS.md`](./BEHAVIORAL_KERNELS.md), [`INTENT_GRAPH_SEMANTICS.md`](./INTENT_GRAPH_SEMANTICS.md), [`MULTI_CHAT.md`](./MULTI_CHAT.md), [`PATCH_LEDGER.md`](./PATCH_LEDGER.md). - -## Problem - -The current interviewer is grounded but slow. It uses a design-decision-tree drilldown strategy: ask phase-shaped questions, walk down the user's design tree, and gradually accumulate enough shared understanding for requirements and criteria. That produces high-provenance intent graph truth, but early users notice the question burden quickly. - -Brunch needs alternative spec-evolution strategies that reduce user burden without weakening the graph into plausible but incoherent generated prose. The FE-705 `brunch agent` / probe-harness branch is therefore not only a CLI feature; it is the first practical strategy test harness. It lets external probes drive the real Brunch lifecycle, generate drilldown-based completed-spec fixtures, and compare alternative strategy outputs before committing product UI. - -## Core model - -A **strategy** is a chat-local policy for advancing semantic state. It decides: - -- what context it reads, -- what question / offer / candidate artifact it produces, -- what output unit it treats as coherent, -- what authority it has to commit graph truth, -- what review or validation must happen before commit, -- what evidence it contributes toward semantic maturity / phase advancement. - -A strategy is not specification-level semantic truth. In the multi-chat model, one specification can have many chats, each with its own strategy and resumable context. - -A Brunch `turn` is assistant/system-first: the assistant/system asks, offers, proposes, or reports something; the user response completes the bundle. Observer/runtime assessment reads the whole bundle, because the assistant/system part gives the user's response its meaning. - -A chat should have at most one open frontier turn. In normal operation, every active/resumable chat should have one open frontier turn, even if it is a scripted frontier such as the first offer in a side-chat. If a chat has no open turn, focusing it may generate a continuation frontier based on chat strategy, chat kind, latest semantic maturity / `phase`, and staleness. - -## Strategy taxonomy - -### Step-by-step drilldown - -Current default. The interviewer asks phase-shaped questions at increasing detail until shared understanding is sufficient. - -- **Strength:** high provenance; each claim is supported by user answers. -- **Weakness:** long and user-burdensome. -- **Commit shape:** incremental canonical changesets after ordinary turn observation / review. - -### Scenario options - -Low-friction strategy for impatient, under-informed, or underspecified users. Brunch asks enough to identify the product/use-case typology, then generates 2–3 coherent candidate graph bundles with named tradeoff profiles. - -- **Strength:** users react to concrete options rather than authoring the whole design. -- **Weakness:** one-shot generation can produce plausible but generic, contradictory, or unsupported graph structure. -- **Commit shape:** candidate graph bundles, accepted cleanly or accepted with explicit open issues. - -### Targeted cases - -Kernel-driven contrastive elicitation from [`BEHAVIORAL_KERNELS.md`](./BEHAVIORAL_KERNELS.md). The interviewer detects active behavioral kernels and asks concrete divergent cases whose classifications emit typed artifacts directly. - -- **Strength:** lower-friction than drilldown, more grounded than whole-spec generation. -- **Weakness:** needs kernel cards, artifact schemas, validators, ordering, and cross-kernel deduplication. -- **Commit shape:** validated kernel artifacts such as decisions, invariants, examples/counterexamples, criteria, and typed edges. - -### Graph review - -Quality-oriented critique that can run over any graph, whether drilldown-created, scenario-generated, imported, or edited. - -- **Question:** where is this graph weak, thin, overconfident, unsupported, ambiguous, generic, uncheckable, or missing structure? -- **Commit shape:** findings start as turn-owned structured artifacts; accepted repairs may later apply changesets. - -### Graph reconciliation - -Repair-oriented process over known disturbance or process debt such as open `reconciliation_need` rows. - -- **Question:** given this specific change/conflict, what existing graph truth must be repaired, confirmed, dismissed, or escalated? -- **Commit shape:** changesets that edit items/edges and/or resolve/open reconciliation needs. - -### Topology-driven targeting - -Internal targeting machinery, not a user-facing strategy for now. Once a graph exists, Brunch can rank next questions, reviews, or repairs by topology: high-fanout low-confidence assumptions, decisions without rejected alternatives, criteria without targets, conflicting constraints, etc. - -## Semantic history and proposal turns - -Turns are conversational provenance and replay. They should not remain the only historical spine once multiple chats, direct edits, review passes, verifier feedback, and candidate bundles can mutate graph truth. - -The future semantic spine is the **changeset ledger**: - -```text -changeset: - one atomic semantic mutation set - -change: - one atomic add/update/link/unlink/retire/etc. inside the changeset -``` - -A changeset mutates a specification from one semantically / structurally valid graph state to another, including any `reconciliation_need` rows opened or resolved by that mutation. The data changes and changeset record must succeed or fail together. The changeset boundary is the smallest atomic unit that preserves semantic coherence: if applying only half the mutation would leave the graph incoherent, it belongs in one changeset. - -A graph-review finding, candidate proposal, or reconciliation suggestion is not itself a changeset until accepted or acted on. It is the assistant/system half of an open frontier turn. The turn completes when the user responds, and only then may the runtime apply a changeset. - -Proposal turns should share a small normalized completion vocabulary: - -- `accept` — authorize the proposal; may apply a changeset. -- `reject` — decline without semantic mutation; narrow because rejection can leave or create process debt. -- `revise` — request a new coherent proposal; maps to labels like "Request changes". -- `ask_followup` — ask for explanation before deciding. -- `defer` — intentionally park the matter. -- `regenerate` — recreate the offer, especially when stale or low-quality. - -Only `accept` applies a proposal turn's semantic changeset. Other proposal actions may create process metadata or successor turns, but should not directly mutate intent graph truth. If a no-edit outcome resolves process debt, model it as accepting a proposal whose changeset resolves the relevant need. - -Proposal/finding artifacts should start as turn-owned structured assistant parts. A standalone proposal or proposed-changeset model should wait until batch review, assignment, expiry, cross-chat surfacing, or independent proposal lifecycle demands it. - -When a turn opens, it should stamp the latest applied changeset id for the specification — for example `turn.opened_at_changeset_id` or `turn.base_changeset_id`. This is not provenance; it is the graph revision the assistant/system offer was based on. First-cut staleness is conservative: if a turn remains open while `specification.latest_changeset_id` advances, the open offer is considered stale and the product offers regeneration / refresh rather than neighborhood-level diffing. - -## Direct editing - -Direct editing is a sibling mutation path, not proposal revision. - -In explicit edit mode, the user may make pending direct changes to one or more intent items. When they exit/apply edit mode, Brunch computes affected incident edges and opens required `reconciliation_need` rows under relation policy; direct item changes and reconciliation needs commit together in one changeset. Direct editing is safe because incoherence risk becomes explicit process debt, not because arbitrary edits are forbidden. - -Review-set direct edits have a special consequence. If the user directly edits proposed review-set items, accepting the review set as-is is no longer valid. `accept` should be disabled; `request changes` becomes a reconciliation-oriented action such as `request reconciliation`. The edited candidate/review set must be reconciled before it can become canonical truth. - -## Relation directionality - -The current `knowledge_edge` relation names mix directionality. `depends_on` and `derived_from` naturally read downstream-to-upstream; `constrains` and `verifies` often read upstream-to-downstream or evidence-to-claim. That becomes risky once edges drive reconciliation. - -FE-700 may break existing relation names/records while expanding the ontology, but forcing every useful edge verb into one dependency direction risks distorting the graph around one operation. The graph must serve display, prompt context, export trace, requirements projection, reconciliation, critique, verification, candidate generation, and explanation. - -Rule: - -> Edge verbs should be semantically clear; operational direction belongs in relation policy. - -Every relation kind should declare: - -- canonical sentence, e.g. `{source} verifies {target}`, -- inverse display sentence, -- graph-display / export / staleness / reconciliation / criteria-help / weak-suggestion participation, -- what happens when source changes, -- what happens when target changes. - -Code should not infer reconciliation behavior from raw edge direction. Direct edit and hard-impact cascade should enumerate incident accepted edges and ask relation policy which endpoint, if any, receives a `reconciliation_need`. - -Contrastive kernels may pressure a further ontology expansion. Kernel questions naturally surface artifacts such as `alternative`, `question`, `ambiguity`, `candidate`, and rejected options. FE-700 should leave room for these artifacts, but the first implementation can represent them as examples, decisions, constraints, or proposal-local structures until durable top-level kinds prove necessary. - -## Candidate graph bundles - -`scenario_options` produces speculative but coherent candidate worlds, not loose item lists. A candidate bundle should contain: - -- short name and scenario summary, -- intended maturity stage, -- tradeoff profile, -- generated items and edges, -- required core items, -- optional/swappable items, -- known risks, -- graph-review findings, -- provenance / epistemic labels, -- commit preconditions. - -User review should be bundle-level by default: `Use this`, `Revise`, `Regenerate`, or ask follow-up. Arbitrary item-level pick-and-choose risks incoherence. Partial acceptance is only safe when the accepted subset is semantically closed or the system brings along required supporting items/edges. - -Candidate readiness should distinguish clean acceptance from acceptance with represented problems: - -- `draft` — generated but not checked, -- `reviewing` — background review running, -- `reviewed_clean` — acceptable normally, -- `reviewed_with_issues` — acceptable only if open issues become durable, -- `blocked` — cannot be accepted without repair/regeneration. - -`reviewed_with_issues` can still be accepted if Brunch durably represents the problems, for example by opening a follow-on graph-review frontier turn or by creating appropriate problem records / `reconciliation_need` rows in the accepting changeset. Imperfect graph states are allowed if their problems are explicit and durable, not hidden. - -Broader graph-review issues should start as turn-owned structured artifacts. `reconciliation_need` remains the only first-class problem table for now, scoped to coherence / staleness process debt caused by relation impacts. A generalized `graph_issue` / `problem` table is a future option if review findings need cross-chat querying, filtering, assignment, badges, or lifecycle independent of turns. - -## Product sequencing - -The most desired product surfaces are: - -1. first-turn strategy choice for a new chat/spec start, -2. a mid-interview "speed this up" / "show me strong options" affordance. - -Engineering still needs part of `graph_review` to make scenario generation credible. `scenario_options` can be the first product-facing strategy while graph review remains an internal oracle used to critique, repair, and score generated bundles before they are committed. - -For mid-interview acceleration, branch into a new or reused side-chat / strategy chat rather than switching the primary interview chat in place. The side-chat branch receives a context pack — not a raw transcript dump — containing spec identity, maturity/phase, summarized goal/context, accepted graph truth, important edge neighborhoods, current frontier question if relevant, unresolved assumptions, and recent turns only when they explain user style or intent. - -The first `speed this up` mode should **complete the current direction**: treat accepted graph truth as fixed premises and fill in plausible missing structure. A more radical "show alternatives that challenge prior assumptions" mode is feasible but deferred. - -Scenario generation should present 2–3 options with named tradeoff profiles. Candidate quality gates should be latency-tiered: - -- fast synchronous gates before display: parse validity, schema validity, coarse fixed-premise check, no obvious contradiction, and tradeoff summary present; -- async gates after display: deeper graph review, coverage, checkability gaps, provenance warnings, repair/refinement. - -The existing observer-style async capture mechanism could generalize into an async semantic worker queue for capture / review / refine / repair. Users can read initial candidates while background review improves readiness. If a candidate is accepted with open issues, Brunch should open or reuse a graph-review chat with a frontier turn summarizing remaining issues and asking what to address first. - -## Concern map and dependencies - -### Semantic substrate — highest coordination - -Owns ontology expansion, relation policy directionality, changeset/change ledger, `turn.opened_at_changeset_id`, `specification.latest_changeset_id`, chat-local strategy metadata, and one-open-frontier-per-chat invariants. - -Likely areas: `src/server/schema.ts`, `src/server/db.ts`, `src/server/knowledge-relationship-policy.ts`, future changeset modules, [`INTENT_GRAPH_SEMANTICS.md`](./INTENT_GRAPH_SEMANTICS.md), [`PATCH_LEDGER.md`](./PATCH_LEDGER.md). - -Sequential dependencies: relation policy before robust reconciliation/direct-edit cascade; changesets before productized candidate acceptance; turn staleness depends on latest changeset tracking. - -### Strategy / proposal artifacts — parallelizable - -Owns candidate bundle shapes, graph-review finding shapes, proposal turn artifacts, candidate statuses, and normalized proposal responses. - -Likely areas: `src/server/parts.ts`, `src/server/turn-artifacts.ts`, a possible `strategy-artifacts` module, context packs, prompt scenarios. - -Can start before durable changesets if artifacts remain turn-owned and do not commit canonical truth. - -### Graph-review oracle — supports scenario options - -Owns review rubric, graph critique prompt, candidate quality gates, accept-with-issues semantics, and follow-on review turns. - -Likely areas: new graph-review prompt/context pack, `src/server/scenario-runner.ts`, `scripts/agent-probes/`. - -Can run probe-only before product UI; needs enough FE-700 ontology/relation policy to be meaningful. - -### Scenario-options strategy — first product-facing acceleration - -Owns 2–3 candidate bundles, tradeoff summaries, fast validation, async review/refine/repair handoff, and clean/with-issues acceptance. - -Likely areas: `src/server/prompts/candidate-spec-system.md`, `src/server/context-pack/candidate-spec.ts`, scenario runner/probe harness, later side-chat UI. - -Depends on graph-review minimum oracle and, for canonical acceptance, changeset ledger. - -### Async semantic workers — staged infrastructure - -Own capture / review / refine / repair background work. Can begin as observer-style in-process tasks before durable queue tables exist. - -### Reconciliation / direct edit — adjacent but distinct - -Owns edit mode, affected-edge enumeration, relation-policy-driven `reconciliation_need` creation, reconciliation chat behavior, and review-set request-reconciliation behavior. - -Likely areas: `src/server/edit-impact.ts`, `src/server/edit-route.ts`, `src/server/reconciliation-need.test.ts`, side-chat/patch-list UI. - -Depends on relation-policy directionality; eventually depends on changesets for atomic direct-edit history. - -## FE-705 implication - -The `brunch agent` JSONL seam is a strategy test harness: - -- drive current drilldown headlessly, -- produce completed-spec fixture candidates, -- preserve workspace state for curation, -- compare strategy outputs against known-good or semi-golden graphs, -- exercise Brunch-owned mutation authority rather than direct DB shortcuts. - -This lets Brunch evaluate strategy outputs before exposing them as product modes. diff --git a/archive/docs/design/SUBSTRATE_STRANGLER_COORDINATION.md b/archive/docs/design/SUBSTRATE_STRANGLER_COORDINATION.md deleted file mode 100644 index b65e4f3bb..000000000 --- a/archive/docs/design/SUBSTRATE_STRANGLER_COORDINATION.md +++ /dev/null @@ -1,196 +0,0 @@ -# Substrate Strangler Coordination - -> Status: **working design proposal / coordination note**, 2026-05-13. -> -> Purpose: keep FE-705 / FE-700 / FE-701 substrate work and parallel frontend/product-surface work moving without forcing an early frontend cutover. Canonical sequencing remains in `memory/PLAN.md`; this document records lane boundaries, collision zones, and the migration rule of thumb. - -## Coordination principle - -Treat the capability / changeset substrate as a **strangler migration**, not a frontend rewrite. - -```text -Frontend today - → existing REST / SSE routes - → shared application handlers - → db stores / schema - -Agent / future capability clients - → capability dispatcher / JSONL adapter - → same shared application handlers - → db stores / schema -``` - -The frontend should not have to switch substrates until the backend has already made the old route substrate an adapter over the new authority. Existing UI routes stay stable while their internals migrate toward shared command/query handlers and changeset-backed semantic writes. - -## Non-goals for the coordination window - -- Do not require current frontend routes to call the central capability adapter. -- Do not expose new changeset fields in user-facing DTOs until a product slice needs them. -- Do not let external agents or probe harnesses write durable graph truth through ORM helpers. -- Do not widen FE-701 into the full ontology expansion; FE-701 needs enough relation-policy directionality to make mutation history safe. - -## Lane split - -### Substrate lane - -Best owned by the agent working on FE-705 / FE-701 backend authority. - -Owns: - -- shared application-service / command-handler seam under existing routes and new capability adapters -- capability parity tests for route path vs capability path -- minimal relation-policy directionality needed by cascade, context snapshots, and changesets -- endpoint-relative relation labels for dependency/dependent snapshot rendering -- item/neighborhood/economic graph context snapshot builders -- `changeset` / `change` schema and stores -- `specification.latest_changeset_id` -- proposal-turn opened/base changeset identity -- `reconciliation_need.caused_by_changeset_id` replacing the historical `caused_by_patch_id` placeholder -- hidden changeset creation under existing semantic mutations before frontend DTO cutover - -Acceptance posture: - -- existing UI behavior and API shapes remain stable unless a scoped product slice explicitly changes them -- semantic writes pass through a shared handler that can be called by both route adapters and capability adapters -- old DB helper access remains internal; capability ids name product operations, not persistence primitives - -### Frontend / product-artifact lane - -Best owned by the colleague working on future-facing UI and low-collision product surfaces. - -Owns: - -- continuous workspace / phase-addressable host work against current read models -- fixture-backed candidate bundle cards and graph-review finding cards -- review status badges and proposal-artifact presentation states -- read-only graph/workspace improvements -- mocked or artifact-only scenario-options UI probes -- UI rendering for snapshot artifacts and endpoint-relative dependency/dependent groups when backed by server fixtures - -Acceptance posture: - -- no canonical graph mutation from candidate/proposal UI until FE-701 changesets exist -- frontend work consumes stable current read models or fixtures, not transitional internal stores -- UI prototypes may model future statuses, but acceptance/apply flows stay disabled, mocked, or explicitly proposal-only - -## High-conflict files and seams - -Coordinate before touching these: - -- `src/server/schema.ts` -- `drizzle/*` -- `src/server/db/*` -- `src/server/knowledge-relationship-policy.ts` -- semantic mutation handlers and edit/reconciliation routes -- turn completion / chat transition logic -- shared API types when changing existing frontend DTOs -- prompt/context pack contracts that become canonical mutation inputs -- context snapshot artifact schemas and mention-resolution contracts - -Lower-conflict frontend work usually lives in: - -- `src/client/components/*` -- Ladle stories and fixtures -- read-only graph/workspace route presentation -- candidate/proposal/graph-review renderers backed by static artifacts -- context snapshot artifact renderers backed by server-generated fixtures - -## Upcoming substrate waves and expected interfaces - -This section is a coordination forecast, not an implementation commitment. The rule remains: existing frontend REST/SSE contracts stay stable until a scoped product slice cuts over. The backend work should first produce server-owned functions and capability contracts that the frontend can treat as future affordances, static fixtures, or debug/probe inputs. - -### Wave 1 — Intent graph semantics and relation policy - -What the substrate lane should provide first: - -| Interface shape | Projected names | Expected use | -| --- | --- | --- | -| Server relation-policy functions | `getRelationPolicy(relation)`, `validateIntentEdge(input)`, `renderIntentEdgeEndpoint(edge, anchorItemId)`, `bucketIntentEdgeForSnapshot(edge, anchorItemId)` | Frontend/context packs can display mixed-direction edges without guessing from `from_item_id` / `to_item_id`. | -| Server graph read/query helpers | `readIntentGraph(specificationId)`, `readIntentItemsById(specificationId, itemIds)`, `readIntentNeighborhood(input)` | Read-only graph and snapshot builders; no mutation authority. | -| Capability contracts | `intentGraph.validateEdge`, later `intentGraph.query`, `intentGraph.renderNeighborhood` | Agent/probe tools for graph reads and edge validation. | - -Frontend expectation: relation wording, dependency/dependent grouping, and reconciliation direction should come from server policy or fixtures generated by that policy. UI code should not hard-code inverse labels such as “is constrained by” by reversing the verb. - -### Wave 2 — Context snapshot builders for chats - -What the substrate lane should provide around chat context: - -| Interface shape | Projected names | Expected use | -| --- | --- | --- | -| Item snapshot builder | `buildIntentItemContextSnapshot({ specificationId, itemIds })` | `#` mentions and explicit item inclusion. | -| Neighborhood snapshot builder | `buildIntentNeighborhoodContextSnapshot({ specificationId, anchorItemIds, mode })` where `mode` starts with `immediate`, `dependencies`, `dependents`, `evidence`, `reconciliation` | Side-chat turn-zero, graph-launched QA, edit-impact previews, reconciliation context. | -| Economic graph snapshot builder | `buildEconomicIntentGraphContextSnapshot({ specificationId, budget })` | New unanchored secondary chats in an existing spec. | -| Snapshot renderer | `renderContextSnapshotArtifact(snapshot)` / context-pack renderer | Produces transcript-visible turn artifacts and compact prompt text. | -| Mention resolver | `resolveIntentItemReferences({ specificationId, refs })` | Server-owned resolution for `#D7` / reference-code mentions. | - -Frontend expectation: new chats or mentions should request/receive replayable snapshot artifacts, not mutate a hidden chat-context table. Whole-graph snapshots are historical context only; they should not create handles for every graph item. Item handles and stale refresh wait for real item versions from the changeset ledger. - -### Wave 3 — Changeset ledger and mutation tools - -What the substrate lane should provide once semantic mutations become first-class: - -| Interface shape | Projected names | Expected use | -| --- | --- | --- | -| Changeset application handlers | `submitChangeset(input)`, `applyAcceptedChangeset(input)`, `readLatestChangeset(specificationId)` | One semantic mutation spine for graph edits, proposal acceptance, reconciliation, and future agent edits. | -| Change variants | `intentItem.create`, `intentItem.updateContent`, `intentItem.retire`, `intentEdge.create`, `intentEdge.delete`, later `contextHandle.refresh` only as process/replay state if needed | Frontend submits product intent, not raw DB updates. | -| Capability contracts | `changeset.submit`, later `changeset.apply`, `changeset.get`, `reconciliationNeed.list`, `reconciliationNeed.proposeResolution`, `reconciliationNeed.applyResolution` | Agent tools stay proposal-only until user/HITL acceptance applies truth. | -| Version reads | `getIntentItemVersion(itemId)` backed by latest applied changeset / item revision | Enables chat handles to refresh only changed subjects. | -| Historical neighborhood builders | `buildHistoricalIntentNeighborhoodSnapshot({ itemId, basis: 'original_capture' | 'last_update' })` | Revives the graph context around the changeset that captured or last updated an item. | - -Frontend expectation: do not implement real `accept`, `apply`, `resolve`, or agent graph-edit flows against ad hoc route writes. Once this wave lands, accepted semantic mutations should return changeset identity, updated graph projection, and any created/updated reconciliation needs. - -### Wave 4 — Agent-facing tool projection - -Capability contracts should project the same server handlers used by routes. Likely agent tool families: - -| Tool family | Authority | Notes | -| --- | --- | --- | -| `intentGraph.*` reads / validation / snapshot rendering | `read_only` | Safe for probes and chat context. | -| `chat.*` start/read/ensure/submit operations | `read_only` / `commit_truth` depending on operation | Existing `chat.getPrimary`, `chat.read`, `chat.ensureReady`, and `turn.submitResponse` are the current foundation. Secondary-chat start/focus should follow this shape. | -| `changeset.submit` | `proposal_only` initially | Lets agents propose graph mutations without committing truth. | -| `changeset.apply` / reconciliation apply tools | `commit_truth` / `commit_process_debt` | Should require explicit user/HITL authority. | -| `workspace.*` / `web.*` context tools | `read_only` | Existing capability registry already names these as safe adapter targets. | - -Adapters may expose different tool names for AI SDK, JSONL, Pi, or CLI ergonomics, but those names must remain projections over Brunch-owned capability ids. - -## Backend migration sequence - -1. Keep current route contracts stable and add regression/parity tests around important UI-facing reads/writes. -2. Extract or name shared application handlers underneath existing Express routes. -3. Point capability/JSONL operations at those same handlers instead of ORM helpers. -4. Add minimal relation-policy directionality needed for direct-edit cascade and reconciliation cause semantics. -5. Add FE-701 changeset/change ledger as hidden substrate. -6. Route existing semantic writes through changeset creation while preserving existing response DTOs. -7. Expose changeset/proposal/staleness fields only through probe/debug/capability surfaces first. -8. Cut over frontend flows one at a time after parity is proven. - -## Frontend-safe work before cutover - -The colleague can work independently on: - -- layout shells, navigation, scroll/focus, and phase section rendering -- read-only graph visibility and status affordances -- candidate bundle and graph-review cards using static fixtures -- `reviewed_clean` / `reviewed_with_issues` / `blocked` visual states as non-mutating artifacts -- storybook/Ladle coverage for future proposal surfaces - -Avoid implementing real `accept`, `accept with issues`, `apply`, or `resolve` UI flows against ad hoc route writes. Those should wait for FE-701 handlers or remain mocked. - -## Cutover rule - -A frontend flow may switch to the new substrate only when all are true: - -1. existing route behavior has a parity test or compatibility assertion; -2. the new handler is the authority behind both route and capability entry points; -3. semantic mutations, if any, produce changeset/change rows atomically; -4. proposal or candidate acceptance has a clear user/HITL authority boundary; -5. rollback/failure behavior leaves graph truth and process debt coherent. - -## Relationship to existing docs - -- `AGENT_MUTATION_SURFACE.md` owns operation naming and agent authority classes. -- `MULTI_CHAT.md` owns shipped chat/reconciliation schema rationale. -- `PATCH_LEDGER.md` owns changeset/change algorithmic pressure under historical patch vocabulary. -- `CONVERSATIONAL_WORKSPACE_RUNTIME.md` owns the umbrella runtime synthesis. -- `INTENT_GRAPH_SEMANTICS.md` owns relation-policy directionality, endpoint-relative labels, and neighborhood snapshot modes. -- `memory/PLAN.md` owns actual frontier ordering. diff --git a/archive/docs/design/ln-skills/EVOLUTION.md b/archive/docs/design/ln-skills/EVOLUTION.md deleted file mode 100644 index 3731f4044..000000000 --- a/archive/docs/design/ln-skills/EVOLUTION.md +++ /dev/null @@ -1,313 +0,0 @@ -# Dev Workflow Evolution — `ln-*` Skills, Spec Registry, and the Convergence Story - -> Status: **working design proposal**. -> Date: 2026-05-07. -> Scope: Brunch's own development methodology — the `ln-*` agent-skill family, the `memory/` ontology that drives it, the operational protocols in `AGENTS.md`, and the long-horizon question of whether and how the dev-layer ontology converges with the product-layer ontology. -> -> This document is **not** part of `memory/SPEC.md` because it does not describe Brunch the product. It is the canonical design home for the **dev layer**: how Brunch is built. Conclusions that affect product behavior should still be promoted into `memory/SPEC.md` through `ln-spec`, but most of the material here describes self-tooling rather than user-facing capability. -> -> Source synthesis: external agent conversations captured in [`docs/archive/design/INTENT_SPEC_EVOLUTION.md`](../../archive/design/INTENT_SPEC_EVOLUTION.md). That synthesis treats both the product layer and the dev layer in the same document; this note splits the dev-layer trajectory out so the layers stop colliding. - -## Why this note exists - -The intent-spec branching conversation produced two parallel trajectories: - -1. A **product-layer** direction — Brunch should evolve from eliciting planning specs toward eliciting intent specs, with progressive checkability, behavioral kernels, semantic edges, and graph-first context. Most of that material has now landed in `memory/SPEC.md` (Requirements 38–41, A77–A87, D125, D134–D142, I109–I112, and the Lexicon entries for `intent graph` / `progressive checkability` / `behavioral kernel` / `context pack` / `scenario runner`), focused design docs (`MULTI_CHAT.md`, `PATCH_LEDGER.md`), or the archived source synthesis (`../../archive/design/INTENT_SPEC_EVOLUTION.md`). - -2. A **dev-layer** direction — the same critique, applied recursively to Brunch's *own* spec workflow. The current `memory/SPEC.md` is doing many jobs at once and the markdown-mediated nature of the document creates real cognitive cost on contributing LLMs. The conversation proposed a file-backed canonical spec registry with deterministic checkers and generated views. None of this has landed anywhere except as a one-line horizon item in `memory/PLAN.md` ("Structured development spec registry"). - -The two trajectories share an ontology vocabulary by accident, not by design. Without a written distinction, every reference to "the spec," "the workflow," or "the ontology" inside the source conversation is ambiguous: is it Brunch the product, or Brunch the development project? This note names the layers explicitly, captures the dev-layer's current shape, sketches the proposed trajectory, and frames the long-horizon convergence question that sits above both. - -## The three layers - -```diagram -╭──────────────────────────────────────────────────────────────────╮ -│ Convergence layer │ -│ Brunch develops Brunch. Shared ontology substrate, shared │ -│ progressive-checkability discipline, shared edge semantics. │ -│ Aspirational. Not committed. │ -╰────────────┬──────────────────────────────────┬──────────────────╯ - │ │ - ╭─────────▼──────────╮ ╭───────▼───────────────╮ - │ Product layer │ │ Dev layer │ - │ │ │ │ - │ What users build │ │ How we build Brunch │ - │ with Brunch. │ │ │ - │ │ │ │ - │ Lives in: │ │ Lives in: │ - │ memory/SPEC.md │ │ AGENTS.md │ - │ memory/PLAN.md │ │ .agents/skills/ │ - │ docs/design/* │ │ ln-*/ │ - │ src/ │ │ memory/SPEC.md * │ - │ │ │ memory/PLAN.md * │ - │ Ontology: │ │ docs/praxis/* │ - │ intent graph, │ │ docs/design/* │ - │ knowledge items, │ │ │ - │ relations, │ │ Ontology: │ - │ reviews, │ │ requirements, │ - │ reconciliation │ │ assumptions, │ - │ needs, … │ │ decisions, │ - │ │ │ invariants, │ - │ Workflow: │ │ criteria, … │ - │ four-phase │ │ │ - │ interview │ │ Workflow: │ - │ │ │ ln-* skill chain │ - ╰────────────────────╯ ╰───────────────────────╯ - * memory/SPEC.md and - memory/PLAN.md are - currently shared - substrate but they - describe Brunch the - built thing, not - Brunch the product - users would use. -``` - -A few things follow from drawing the layers explicitly: - -- **`memory/SPEC.md` is dev-layer infrastructure that happens to describe a product.** It is not the product's own ontology surface. When a future Brunch user opens a Brunch project, they do not see `memory/SPEC.md`; they see their own intent graph. The naming overlap (both the dev layer and the product layer use words like *requirement*, *assumption*, *decision*, *invariant*, *criterion*) is convergence pressure, not current convergence. - -- **The `ln-*` skills are the dev-layer workflow.** They are the analog of Brunch's four-phase interview, but for our team's spec-building. The product's interview produces a user's intent graph; the `ln-*` chain produces the canonical state of `memory/SPEC.md` and `memory/PLAN.md`. - -- **`AGENTS.md`** sits above both as repo-level operational protocol — it owns verification harness conventions, branch-and-tracker conventions, and the canonical pointer to where each layer's truth lives. - -The rest of this document focuses on the dev layer. - -## Dev layer — current shape - -The dev workflow today is a markdown-mediated discipline executed by agent skills against canonical files in `memory/`. It works, but the workings are not collected anywhere. - -### The `ln-*` skill family - -The skills at `.agents/skills/ln-*/` form a chain organized by purpose: - -```diagram -╭──────────────╮ ╭──────────╮ ╭──────────╮ ╭────────────╮ -│ Knowledge │ │ ln-grill │──▶│ ln-spec │──▶│ ln-plan │ -│ │ ╰──────────╯ ╰──────────╯ ╰─────┬──────╯ -│ │ ▼ -│ │ ╭────────────╮ -│ │ │ ln-oracles │ -╰──────────────╯ ╰────────────╯ - -╭──────────────╮ ╭──────────╮ ╭──────────╮ ╭────────────╮ -│ Execution │ │ ln-scope │──▶│ ln-spike │──▶│ ln-build │ -╰──────────────╯ ╰──────────╯ ╰──────────╯ ╰────────────╯ - -╭──────────────╮ ╭───────────╮ ╭──────────────╮ ╭──────────╮ -│ Quality │ │ ln-review │──▶│ ln-refactor │──▶│ ln-sync │ -╰──────────────╯ ╰───────────╯ ╰──────────────╯ ╰──────────╯ - -╭──────────────╮ ╭─────────────╮ ╭─────────────╮ ╭───────────╮ -│ Process │ │ ln-consult │ │ ln-handoff │ │ ln-design │ -╰──────────────╯ ╰─────────────╯ ╰─────────────╯ ╰───────────╯ -``` - -Per `AGENTS.md`, the verification boundary is split: `ln-spec` owns the inner-loop verification policy; `ln-oracles` owns middle/outer-loop verification strategy and blind-spot assessment; `ln-scope` applies the oracle strategy per slice; `ln-review` audits oracle coverage. - -### Ontology in use - -The dev-layer ontology in `memory/SPEC.md` today maps roughly to: - -| Kind | Where it lives in `memory/SPEC.md` | -| --- | --- | -| Concept / goal | Concept & Goal section | -| Constraint / non-goal | Constraints & Non-goals | -| Requirement | Requirements (numbered list) | -| Assumption | Assumptions table (with confidence, status, depends-on, validation approach) | -| Decision | Decisions section (numbered, with rationale and dependencies) | -| Invariant | Critical Invariants table (with protected-by tests and proves-which-requirement column) | -| Criterion | Acceptance Criteria section + Verification Design | -| Term | Lexicon (Core terms + Boundary terms) | -| Verification stance | Verification Design (commands, policy, stance, diagnostic assessment, oracle strategy by loop tier, blind spots, current coverage) | - -The ontology is richer than the product layer's current ontology, but it lives in markdown, which means: - -- Every contributing LLM must parse a 600-line document to make a local change. -- Cross-reference maintenance (a decision's `Depends on:` field, an invariant's `Protected by:` field, a requirement's traceability list) is textual and fragile. -- Retirement, supersession, and validation status require editorial discipline rather than tool-enforceable transitions. -- Consistency is checked by rereading, not by querying. -- Generated outputs (no `AGENT_BRIEF.md`, no `VERIFY_MAP.md`, no task-local slices) do not exist; every agent gets the whole file or nothing. - -### Outer loop (Linear + Graphite) - -`AGENTS.md` defines the outer loop: one frontier item in `memory/PLAN.md` becomes one Linear issue in the FE/brunch project and one Graphite stacked branch. Sub-slices stay on the same issue and branch unless `ln-plan` explicitly elevates them. Branch naming: `{prefix}/{issue-id}-{keywords}`. PR title: `{issue-id | upper}: {title in sentence case}`. PR descriptions written only when tying off. - -This outer loop is solid and is not what's under design pressure. The pressure is on the markdown substrate. - -## Pressure points that are real today - -These are **observed today**, not anticipated: - -1. **`memory/SPEC.md` is doing eight jobs at once.** Per the source synthesis: human-readable product narrative, agent-readable current truth, decision register, verification map, glossary, architecture model, test coverage index, and working memory for coding agents. Each new requirement, assumption, or invariant adds load to all eight jobs simultaneously. - -2. **Editorial discipline is the only consistency mechanism.** Retired decisions vanish only if someone retires them. Stale assumptions persist if no one re-reviews them. Requirements pointing at deprecated terms are caught by reading. - -3. **No task-local slices exist.** A coding agent working on, say, the multi-chat substrate has to load all of SPEC.md, all of PLAN.md, three sibling design docs, plus the code — and re-derive the relevant subset every time. There is no `slice --tag multi-chat` analog to `git log -- path/`. - -4. **No `AGENT_BRIEF.md` exists.** New agents pick up the whole spec or nothing. The "global non-negotiables, current architecture seams, active invariants, verification commands" subset that almost every agent needs is not separated from the wider register. - -5. **Cross-reference rot.** The Critical Invariants table's `Protected by:` test names are validated only by running the tests; the requirement traceability column (`Proves`) is validated only by reading. A renamed test or retired requirement creates silent rot. - -6. **Markdown formatting load.** When an LLM must update a 600-line markdown file with column alignment, table formatting, and footnote references, large parts of its context window go to formatting rather than reasoning. - -The point is not that the current system is broken — it works, and `ln-sync` exists precisely to absorb periodic housekeeping. The point is that the marginal cost of every new claim is rising, and the correct fix is to externalize the deterministic parts onto a tool rather than continually re-investing LLM attention. - -## Proposed dev-layer trajectory - -The trajectory is the one the source synthesis captures in §10–11 of [`INTENT_SPEC_EVOLUTION.md`](../../archive/design/INTENT_SPEC_EVOLUTION.md), but framed here as a self-tooling experiment for *this* repo, not as a product proposal. - -### Target shape - -``` -memory/spec/ - schema/ - record.schema.json - relation.schema.json - records/ - goals.yaml - context.yaml - constraints.yaml - assumptions.yaml - decisions.yaml - requirements.yaml - invariants.yaml - criteria.yaml - examples.yaml - terms.yaml - verification.yaml - generated/ - SPEC.md # human-readable, never edited directly - AGENT_BRIEF.md # compact, agent-facing, almost always loaded - VERIFY_MAP.md # invariant → test → requirement coverage - OPEN_RISKS.md # open assumptions, stale items, gaps - tools/ - check.ts # deterministic checker - render.ts # records → generated views - slice.ts # records → task-local slice for a tag/area -``` - -The split is between **canonical** (small typed records, one per claim) and **rendered** (disposable generated markdown views for humans and agents). The agent's view of the world becomes: - -- Always: `AGENT_BRIEF.md` (compact non-negotiables + invariants + verification commands) -- Per task: `slice --tag ` (relevant requirements, decisions, invariants, criteria, open assumptions for that area) -- Rarely: the whole rendered `SPEC.md` - -### The "for any change" contract - -Once the trajectory begins, the contract on a contributing agent becomes: - -1. Load `AGENT_BRIEF.md` plus a task slice. -2. Preserve the named invariants flagged in the slice. -3. Update structured records (`memory/spec/records/*.yaml`), never the generated markdown. -4. Run `npm run spec:check` (joined into the existing `npm run verify` gate per AGENTS.md's verification harness). - -### Migration path (5 steps) - -A staged on-ramp from the source synthesis, adapted to this repo's reality: - -1. **Stable IDs and front-matter on every existing claim.** Every requirement, assumption, decision, invariant, criterion already has a stable code in `memory/SPEC.md` (Requirement 39, A82, D138, I111, etc.). Confirm coverage; introduce IDs for any items that lack them. - -2. **Sidecar files alongside the markdown.** Begin with `memory/spec/records/*.yaml` populated from the existing markdown without deleting the markdown. Both views exist; the markdown remains canonical during the transition. - -3. **Stop editing generated markdown.** Once the renderer can produce the markdown faithfully, the markdown becomes generated. The records become canonical. Editing the markdown directly is a `spec:check` violation. - -4. **Spec checks integrated into the verify gate.** `npm run verify` adds `spec:check` after `test` and `build`. Failures from dangling references, missing oracles, retired-records-in-active-views, etc. block the gate the same way a failing test does. - -5. **Task-local slices for agent context.** `slice --tag multi-chat` produces a markdown slice that the `ln-build` skill loads instead of the whole spec. `AGENT_BRIEF.md` becomes the always-loaded preamble for every skill in the chain. - -### Tool vs. direct edit policy - -From the source synthesis: "records editable, tool preferred, checker authoritative, generated never edited." - -A staged approach to mutation interface: - -1. **Stage 1**: agents may edit YAML records directly; `spec:check` validates structure. -2. **Stage 2**: common semantic mutations move behind CLI commands (`spec add --kind invariant`, `spec retire DEC-128 --superseded-by DEC-141`, `spec link CRIT-012 verifies INV-024`). Direct edits remain possible for humans. -3. **Stage 3**: CI rejects invalid registry state; agents prefer tools. - -The sequence matters: don't build the CLI until the records exist; don't build CI rejection until the CLI exists; don't deprecate direct edits until both exist. - -### What `spec check` enforces - -Candidate checks (also from the source): - -- No dangling relation targets. -- No duplicate IDs. -- Every requirement has at least one criterion or an explicit verification gap. -- Every criterion verifies at least one requirement or invariant. -- Every invariant has an oracle, or is marked manual / proof-candidate / gap. -- Every active decision has rationale and affected scope. -- Every assumption has a validation approach or retirement condition. -- No retired record appears in active generated views. -- No forbidden legacy term appears outside glossary aliases. - -These are the cheapest deterministic checks that today only happen if a human reads the whole document carefully. - -## Convergence layer (long horizon) - -The convergence question sits above both layers: should the **dev-layer ontology** (what we maintain in `memory/SPEC.md`) and the **product-layer ontology** (what users build with Brunch) eventually share a substrate? - -The structural argument for convergence is strong: - -- They share kind names (requirement, assumption, decision, invariant, criterion, example, term). -- They share relation semantics (`depends_on`, `derived_from`, `constrains`, `verifies`, `refines`, `illustrates`). -- They share progressive-checkability discipline: each claim should receive the weakest sufficient witness. -- They share the "LLM proposes; deterministic systems own structure" governance pattern. - -The structural argument against immediate convergence is also strong: - -- They have different persistence needs. The dev layer is diffable, branchable, reviewable in PRs — files. The product layer is interactive, multi-user, resume-precise — SQLite. (Source: [`INTENT_SPEC_EVOLUTION.md`](../../archive/design/INTENT_SPEC_EVOLUTION.md) §11.) -- They have different mutation interfaces. The dev layer mutates through editor + CLI. The product layer mutates through interview turns, observer captures, and graph edits. -- They have different operational metadata. The dev layer cares about test coverage and CI gates; the product layer cares about workflow phase, frontier ownership, review acceptance, and chat ownership. - -The unifying principle the source proposes: - -``` -packages/spec-ontology/ - kinds.ts # KnowledgeKind discriminated union - relations.ts # RelationKind + relation-policy registry - schemas.ts # shared zod / typed schemas - validators.ts # cross-kind invariants - projectors.ts # render → markdown / graph / brief - -SQLite adapter: - product runtime state - -File adapter: - dev registry, fixtures, exports - -Markdown projector: - human/agent-readable docs (both layers) -``` - -The decision rule: - -> If humans and agents should review it in Git, use files. -> If the running app needs to mutate it interactively and resume precisely, use SQLite. -> The ontology is the same; the adapters differ. - -**Brunch develops Brunch** is the strongest form of this convergence: at some future point, Brunch the product can interview *itself* — the dev team sits in front of the same app users sit in front of, and the resulting intent graph *is* `memory/SPEC.md`. That is not committed. It is a north star that organizes the smaller decisions: every time we sharpen the product ontology in a way that does not work for the dev ontology (or vice versa), we are accumulating convergence debt. - -## Open questions - -- **Substrate format.** YAML records vs. JSONL records vs. markdown-embedded `spec-record` fenced blocks? YAML is most readable, JSONL is most append-friendly, embedded blocks let humans edit alongside narrative. The source recommends YAML; this repo's existing markdown discipline may favor embedded blocks during the transition. - -- **CLI mutation precedence.** Which mutations deserve a CLI command first? Likely `add`, `link`, `retire`, then `slice` and `render`. `supersede` and `mark stale` are more complex and may stay manual longer. - -- **`AGENT_BRIEF.md` contents.** What goes in the brief vs. a task-local slice? Candidates for the brief: product thesis, global non-negotiables, current architecture seams, active invariants, verification commands, and "for any change" rules. Candidates for slices: requirements/decisions/invariants/criteria scoped to one area. - -- **First adopter.** Should the registry experiment start with the full `memory/SPEC.md` or with one bounded sub-area (e.g. only the multi-chat substrate's records)? Bounded is cheaper to abandon if the experiment fails. - -- **Convergence commitment.** Should the convergence layer become a real planning commitment (a stub `packages/spec-ontology/` shared by both adapters), or should it remain a north star until the product ontology stabilizes further? - -- **Skill rewrites.** Once the registry exists, do the `ln-*` skills move from "edit markdown" to "run `spec` commands"? `ln-spec` becomes `spec add --kind `; `ln-sync` becomes `spec retire`/`spec render`; `ln-review` becomes `spec list --status open --confidence low`. This is a significant skill-rewrite, and may itself be the right pilot for the registry. - -- **What does *not* migrate.** Some `memory/SPEC.md` sections are genuinely narrative — Concept & Goal, Verification Design's prose explanations, Lexicon definitions. These may stay as markdown-with-front-matter rather than fully decomposing into records. The split between "structured claim" and "narrative passage" is itself a design question. - -## References - -- [`INTENT_SPEC_EVOLUTION.md`](../../archive/design/INTENT_SPEC_EVOLUTION.md) §10–11 — source synthesis for the registry trajectory and the persistence adapter split. -- [`AGENTS.md`](../../../AGENTS.md) — current operational protocols, verification harness, naming conventions. -- `.agents/skills/ln-*/SKILL.md` — current implementations of the dev-workflow skills. -- `memory/PLAN.md` horizon item "Structured development spec registry" — the one-line pointer this document expands. diff --git a/archive/docs/design/ln-skills/README.md b/archive/docs/design/ln-skills/README.md deleted file mode 100644 index 1432cbb3e..000000000 --- a/archive/docs/design/ln-skills/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# ln-skills Design Notes - -This directory holds design rationale for Brunch's `ln-*` agent-skill workflow and related dev-layer self-tooling. - -These documents are not executable skills. Runtime skill instructions live under `.agents/skills/ln-*/`; accepted operational protocols belong in `AGENTS.md` or `docs/praxis/`; canonical product truth remains in `memory/SPEC.md` and `memory/PLAN.md`. - -| Document | Role | -| --- | --- | -| `EVOLUTION.md` | Dev-layer trajectory for the `ln-*` skill family, `memory/` ontology, proposed file-backed spec registry, and possible dev/product ontology convergence. Not product SPEC. | diff --git a/archive/docs/design/state-machines/README.md b/archive/docs/design/state-machines/README.md deleted file mode 100644 index 5aba1c6e0..000000000 --- a/archive/docs/design/state-machines/README.md +++ /dev/null @@ -1,429 +0,0 @@ -# Turn-Based State Machines - -State machine design for Brunch's turn-based workflow. Written for Stately Studio -(studio.stately.ai); code is XState v5 and paste-ready. - -This document is downstream of the current product/data-model spec in -`memory/SPEC.md`. In particular: - -- kickoff and recovery are **projected control cards**, not authored durable turns -- the workspace stream is a **merged read model**, not identical to the turn tree -- open phases must bottom out in exactly one visible next action -- recovery is a **structural fallback**, not another turn that must be generated - -The charts therefore model workflow legality and product-visible state, while a -small runtime layer owns hydration, cancellation, queue recovery, and stale-event -suppression. - -For a companion clarification of **workflow projection (read path)** vs -**workflow transition/orchestration (write path)**, see -`docs/archive/design/WORKFLOW_OWNERSHIP.md`. - -## Why State Machines - -The durable model in `SPEC.md` is now much clearer about product meaning: turns -own conversational lineage, phase outcomes are anchored workflow facts, and -kickoff/recovery/handoff controls project from workflow state. What still needs a -runtime design is the orchestration around an open phase: - -- reply submission and interviewer processing -- successor generation and visible generation state -- observer capture backlog and late capture attachment -- phase-boundary durable writes -- hydration after reload or crash -- force close, cancellation, and late-event races - -Those interactions still have enough concurrency and enough invalid-state risk to -be worth modeling explicitly. Statecharts keep the phase workflow legible. The -runtime host keeps the charts from turning into procedural control flow. - -## Recommended Shape - -The preferred shape is a **runtime supervisor over slim charts**, combined with a -**recovery-first reconciler** for hydration. - -That means: - -- durable truth remains authoritative -- hydration lands from a pure reconciliation function -- live interviewer/generation work is ownable and cancellable -- stale outputs are ignored through ephemeral leases plus durable idempotency -- restart favors safe reconciliation over pretending a lost in-flight operation survived - -It does **not** mean introducing a second durable workflow model or making kickoff -and recovery durable turn categories again. - -## Topology - -``` -durable snapshot -├── active-path turns -├── anchored phase outcomes -├── accepted review outputs -└── turn capture statuses - │ - ▼ -deriveSpecificationLanding(snapshot) ← pure reconciliation - │ - ▼ -specification runtime ← owns leases, queues, cancellation, -├── spec machine stale-output rejection, retries -│ ├── loading -│ ├── phase_running -│ ├── recording_phase_outcome -│ ├── boundary_write_failed -│ └── complete -│ -└── phase machine ← owns in-phase workflow legality - ├── awaiting_kickoff ← projected kickoff control card - ├── active - │ ├── awaiting_reply - │ ├── interviewer_processing - │ ├── generating_successor - │ └── awaiting_recovery ← projected recovery control card - ├── closed_via_interviewer - └── closed_via_force -``` - -The phase machine is still a child of the spec machine. The important change is -that the spec machine is no longer asked to encode every durability and lifecycle -detail itself; those concerns sit in a runtime host around the charts. - -## Public Runtime Boundary - -```ts -interface SpecificationRuntime { - start(snapshot: DurableSpecificationSnapshot): RuntimeView; - send(event: RuntimeEvent): void; - stop(): Promise; -} - -type RuntimeView = { - landing: SpecificationLanding; - actor: ActorRef; -}; -``` - -The public boundary is intentionally small. Callers should not need to know about -queue reseeding, cancellation, leases, or retry policy. - -## Landing Derivation - -Hydration must land from durable truth, not from optimism about lost live work. - -```ts -type SpecificationLanding = - | { kind: 'projected_kickoff'; phaseKey: PhaseKey } - | { - kind: 'frontier_turn'; - phaseKey: PhaseKey; - turnId: TurnId; - turnKind: DurableFrontierTurnKind; - } - | { - kind: 'visible_generation'; - phaseKey: PhaseKey; - answeredTurnId: TurnId; - successorKind: SuccessorKind | null; - } - | { kind: 'projected_recovery'; phaseKey: PhaseKey; reason: RecoveryReason } - | { kind: 'handoff'; closedPhaseKey: PhaseKey; nextPhaseKey: PhaseKey | null } - | { kind: 'complete' }; - -function deriveSpecificationLanding( - snapshot: DurableSpecificationSnapshot, -): SpecificationLanding; -``` - -This function is the authoritative hydration rule. It should be pure and heavily -tested. It derives the one visible bottom artifact from: - -- active-path turn lineage -- anchored phase outcomes -- accepted review carry-forward state -- durable capture status -- any durable evidence that justifies a visible generation state - -### Recovery-First Reconciliation - -On restart, prefer **safe reconciliation** over exact mid-flight revival. - -- If durable state proves an unanswered frontier turn exists, land in `frontier_turn`. -- If durable state proves the phase is in entry state, land in `projected_kickoff`. -- If durable state proves the phase is closed, land in `handoff` or `complete`. -- If durable state cannot prove a valid frontier after an answered turn, land in - `projected_recovery`. - -Do not reopen into a fake `visible_generation` state just because the process died -mid-request. `visible_generation` is primarily a live-runtime state and should only -hydrate if future durable evidence makes that truthful. - -## Division Of Responsibility - -| Concern | Owner | Why | -| ------- | ----- | --- | -| Landing derivation on hydration/restart | `deriveSpecificationLanding` | Keeps restart semantics pure, explicit, and testable | -| In-phase workflow legality | phase machine | Small chart with product-visible open-phase states | -| Phase-to-phase progression and boundary retries | spec machine | The spec is the only place with the linear cross-phase view | -| Interviewer / generation lifecycle | runtime supervisor | Start, cancel, and stale-event suppression are live-operation concerns | -| Observer backlog reseeding and dispatch | runtime supervisor | Queue internals do not belong in the chart, but backlog truth must survive restarts | -| Stream projection / UI | read model assembly | The stream is derived from durable turns, anchored facts, projected controls, and activity cards | - -The rule of thumb is: if a concern is about **what workflow state is legal and -visible**, it belongs in the charts; if it is about **how asynchronous work is -owned, retried, canceled, or reconciled**, it belongs in the runtime. - -## Key Invariants Made Structural - -1. **Exactly one bottom artifact in an open phase.** An open phase must bottom out - in one and only one of: projected kickoff, frontier turn, visible generation, - or projected recovery. -2. **No open phase without a visible next action.** There is no silent state in - which a phase is open but the user sees neither an actionable frontier nor a - truthful structural fallback. -3. **Phase cannot be closed without a durable outcome.** Closing the phase actor is - not enough; the spec machine must record the phase outcome before the next phase - can be considered open. -4. **Next phase opens into projected entry, not a required kickoff row.** The next - phase's entry affordance is a projected control card. Any durable helper seam is - transitional implementation detail, not product truth. -5. **Reply submission is atomic with interviewer kickoff.** There is no state where - the reply was accepted but interviewer processing has not yet begun. -6. **Observer capture stays turn-owned.** Capture status always belongs to the - answered turn that just left the frontier; late observer completion reattaches to - that same turn card on replay. -7. **Late live outputs are ignorable.** Force-close, phase transition, or runtime - stop must make stale interviewer/generation outputs harmless through leases and - idempotent durable writes. - -### Bug Class To Fold In - -One concrete bug class to retire in this work: observer/capture state must remain -owned by the answered turn that triggered it, rather than leaking onto the -successor turn or depending on loosely coordinated UI flags. The failure pattern -shows up as answered cards skipping their transient `still thinking` state, -remaining stuck there forever, or inheriting stale `data-observer-result` -artifacts that suppress later observation for the wrong turn. The eventual state -model should make per-turn observer lifecycle explicit and reject cross-turn -observer artifact attachment as invalid. - -## Projected Controls In The Charts - -`awaiting_kickoff` and `awaiting_recovery` stay as real workflow states, but their -visible artifact is a **projected control card**, not a durable authored turn. - -That means: - -- the phase machine may still name `awaiting_kickoff` -- the phase machine may still name `awaiting_recovery` -- neither state implies a required `kickoff` or `recovery` row in the `turn` table -- leaving those states should result in a normal durable conversational turn or a - phase closure, not in a special durable control-turn category - -This keeps the chart expressive without regressing D94/D95/D110. - -## Live Operation Ownership - -The runtime supervisor should own live interviewer/generation work through a small, -ephemeral lease registry. - -```ts -type OperationLease = - | { - kind: 'interviewer'; - leaseId: string; - phaseKey: PhaseKey; - frontierTurnId: TurnId; - } - | { - kind: 'successor'; - leaseId: string; - phaseKey: PhaseKey; - answeredTurnId: TurnId; - } - | { - kind: 'boundary_write'; - leaseId: string; - phaseKey: PhaseKey; - write: 'recording'; - }; -``` - -Leases are runtime-local. They exist to: - -- cancel in-flight work on force close or phase exit -- reject stale outputs from superseded work -- keep the charts free of cancellation bookkeeping - -Leases are **not** the recovery source of truth across restarts. Durable truth plus -idempotent writes carry that burden. - -## Observer Backlog: Durable, Not Just In Memory - -The observer itself is still a stateless `generateObject` call. What needs durable -modeling is the backlog: which answered turns still need capture, which failed, and -which already succeeded. - -The current implementation may still use `p-queue`, but the durable model should -carry per-turn capture status. At minimum: - -- `pending` -- `succeeded` -- `failed` - -If implementation detail benefits from `in_progress`, that can exist, but restart -truth must be recoverable from durable state. - -Hydration should re-seed the queue from durable turns whose capture status is not -yet settled. `SPEC_HYDRATED` should therefore carry both: - -- the derived `landing` -- the list of turn ids whose capture should be re-enqueued - -## Write Ordering And Idempotency - -The runtime must pin the ordering between durable writes and chart events. - -Rules: - -- A successor event is emitted only after the successor turn is durable. -- A phase-outcome success is emitted only after the anchored outcome write lands. -- Capture success/failure is emitted only after the answered turn's durable capture - status has been updated. - -These writes should also be idempotent around real domain seams: - -- successor creation keyed by the answered frontier turn and continuation slot -- phase-outcome recording keyed by the authoritative closure anchor on the active path -- observer capture keyed by answered turn id - -This is enough to make retries safe without introducing a general durable operation -ledger. - -## Closure Rejection - -Closure rejection remains the normal reply path. - -- a closure proposal is still a durable proposal-shaped conversational turn -- rejecting it is still `REPLY_SUBMITTED` -- the interviewer must then produce a same-phase successor frontier -- the runtime/integration layer should prevent immediate blind re-proposal loops - -The chart does not need a separate `CLOSURE_REJECTED` event unless implementation -clarity later proves it worthwhile. A comment plus tests is enough for now. - -## Late Events And Force Close - -XState serializes events per actor, but external work can still resolve late. - -Important cases: - -- `REPLY_SUBMITTED` racing with `FORCE_CLOSE_REQUESTED` -- `INTERVIEWER_DECIDED` arriving after a force close -- `SUCCESSOR_GENERATED` arriving after the phase already moved on - -The intended rule is simple: the runtime should cancel in-flight work where -possible, and any output carrying an expired lease should be ignored. Durable -idempotency prevents already-completed writes from corrupting state when retries or -late completions happen. - -## Routing Integration - -Spec runtimes are specification-owned, not route-owned. Route components subscribe -to the actor/runtime view; they do not create or destroy the authoritative runtime. - -Consequences: - -- navigating away from the active phase does not kill interviewer work -- closed phases have no live phase actor; their routes read durable data and the - projected handoff state -- at most one phase actor is alive per specification at a time -- the runtime is a third source of live state alongside durable router loaders and - cached reads, and should be named as such in the codebase - -## Naming Conventions - -**States.** Use gerunds for active work (`loading`, `generating_successor`), -`awaiting_*` for durable conditions or waits (`awaiting_reply`, `awaiting_kickoff`, -`awaiting_recovery`), and adjectives / past participles for terminal states -(`closed_via_*`, `complete`, `boundary_write_failed`). - -**Events.** Prefer `_` for facts (`REPLY_SUBMITTED`, -`INTERVIEWER_DECIDED`, `SUCCESSOR_GENERATED`, `CAPTURE_SUCCEEDED`). Deliberate -command-style exceptions are acceptable when the user or runtime is issuing a -request rather than reporting a completed fact (`BOUNDARY_RETRY_REQUESTED`, -`FORCE_CLOSE_REQUESTED`). - -## Deferred / Later Work - -- **`generating_successor` substates.** Split into thinking/tool-use/streaming only - if the UI needs that level of visible distinction. -- **Workspace-level runtime registry.** Probably a lightweight registry of - specification runtimes rather than a workspace-wide super-machine. -- **Revisit and secondary threads.** Still outside these charts; likely a separate - small lifecycle machine later. -- **Durable runtime-operations table.** Only introduce if restart-resumable long - jobs or multi-process coordination becomes necessary. - -## Relation To The Projector Frontier - -This design note is the machine/runtime articulation of the merged stream -projector frontier in `memory/PLAN.md`. - -That frontier depends on four explicit contracts: - -- a pure `deriveSpecificationLanding(snapshot)` reconciler that produces the one - truthful bottom artifact for hydration and resume -- a narrowed `OpenPhaseLanding` contract that feeds the phase chart only open-phase - states -- a slim spec chart that owns only cross-phase legality and `phaseOutcome` retry -- a runtime host that owns queue reseeding, leases, cancellation, stale-event - rejection, and write-ordering discipline - -This is how the projector cutover stops treating kickoff/recovery as durable turn -truth and starts treating them as projected controls over anchored workflow facts. - -## Refactor Targets For Current Drafts - -The current draft files in this directory still reflect the older kickoff/recovery -as-turn model. The next refactor should move them toward this document, not the -other way around. - -### `spec-machine.ts` - -- replace `SPEC_HYDRATED { activePhaseKey, kickoffTurnId }` with a landing union and - pending capture ids -- remove `seedKickoffTurn` / `seeding_next_kickoff` as a required product seam -- keep `recording_phase_outcome` as the real phase-boundary durable write -- push queue ownership, lease management, and stale-event rejection into a runtime - host around the chart -- narrow the child-phase input to open-phase landing states rather than handing it a - kickoff turn id -- derive handoff/completion from `phaseOutcome` and landing reconciliation rather - than treating them as second chart-owned boundary writes - -### `phase-machine.ts` - -- keep `awaiting_kickoff` and `awaiting_recovery` as states, but stop treating them - as durable turn kinds -- remove `kickoff` / `recovery` from the durable turn-kind authority -- replace `RECOVERY_GENERATED` semantics with a projected recovery control that - leads back to a normal successor-frontier path -- keep closure rejection on the normal reply path -- initialize from narrowed open-phase landing input, not from an assumed kickoff - turn id - -## Suggested Refactor Sequence - -1. Define `SpecificationLanding`, `OpenPhaseLanding`, and - `deriveSpecificationLanding(snapshot)` in design/shared types. -2. Introduce the specification runtime host around the charts. -3. Rewrite the chart drafts so hydration and restart flow through landing unions. -4. Cut server/client projector, fixtures, and tests over to projected controls and - derived landings. - -## Files - -- `phase-machine.ts` — phase frontier machine draft -- `spec-machine.ts` — spec-level machine draft -- `README.md` — current design authority for the machine/runtime seam diff --git a/archive/docs/design/state-machines/phase-machine.ts b/archive/docs/design/state-machines/phase-machine.ts deleted file mode 100644 index 0cbed7938..000000000 --- a/archive/docs/design/state-machines/phase-machine.ts +++ /dev/null @@ -1,252 +0,0 @@ -// Phase frontier state machine for Brunch. -// Paste-ready for Stately Studio (studio.stately.ai) — XState v5. -// -// One instance per open phase (spawned as an invoked child by the spec chart). -// Pure in-memory orchestration: no durable writes. This chart owns only in-phase -// legality and visible open-phase states. The runtime host around the spec chart -// owns durable landing reconciliation, write ordering, leases, and stale-event -// rejection. - -import { assign, setup } from 'xstate'; - -type TurnId = string; - -type PhaseKey = 'grounding' | 'design' | 'requirements' | 'criteria'; - -type DurableFrontierTurnKind = 'question' | 'grounding' | 'review' | 'closure'; - -type SuccessorKind = 'question' | 'grounding' | 'review' | 'closure_proposal' | 'accept_close'; - -type RecoveryReason = 'generation_failed' | 'frontier_missing' | 'frontier_invalid'; - -type OpenPhaseLanding = - | { kind: 'projected_kickoff'; phaseKey: PhaseKey } - | { - kind: 'frontier_turn'; - phaseKey: PhaseKey; - turnId: TurnId; - turnKind: DurableFrontierTurnKind; - } - | { - kind: 'visible_generation'; - phaseKey: PhaseKey; - answeredTurnId: TurnId; - successorKind: SuccessorKind | null; - } - | { kind: 'projected_recovery'; phaseKey: PhaseKey; reason: RecoveryReason }; - -type ClosureBasis = 'interviewer' | 'force'; - -type Context = { - landingKind: OpenPhaseLanding['kind']; - phaseKey: PhaseKey; - frontierTurnId: TurnId | null; - frontierTurnKind: DurableFrontierTurnKind | null; - lastAnsweredTurnId: TurnId | null; - pendingSuccessorKind: SuccessorKind | null; - generationFailure: string | null; -}; - -type Event = - | { type: 'KICKOFF_ACCEPTED' } - | { type: 'REPLY_SUBMITTED'; turnId: TurnId; payload: unknown } - | { type: 'FORCE_CLOSE_REQUESTED' } - | { type: 'INTERVIEWER_DECIDED'; successorKind: SuccessorKind } - | { type: 'SUCCESSOR_GENERATED'; turnId: TurnId; turnKind: DurableFrontierTurnKind } - | { type: 'GENERATION_FAILED'; reason: string } - | { type: 'RECOVERY_CONTINUED' }; - -type Output = { basis: ClosureBasis }; - -export const phaseMachine = setup({ - types: { - context: {} as Context, - events: {} as Event, - input: {} as { landing: OpenPhaseLanding }, - output: {} as Output, - }, - actions: { - // Fire-and-forget: tell the parent spec chart that a durable frontier turn was - // answered. The runtime host decides how to enqueue observer capture. - emitTurnAnswered: (_, _params: { turnId: TurnId }) => { - // sendParent({ type: 'TURN_ANSWERED', turnId: params.turnId }) - }, - }, - guards: { - isAcceptClose: ({ event }) => - event.type === 'INTERVIEWER_DECIDED' && event.successorKind === 'accept_close', - isClosureProposal: ({ event }) => - event.type === 'INTERVIEWER_DECIDED' && event.successorKind === 'closure_proposal', - isGenerativeSuccessor: ({ event }) => - event.type === 'INTERVIEWER_DECIDED' && - (event.successorKind === 'question' || - event.successorKind === 'grounding' || - event.successorKind === 'review'), - startsAtProjectedKickoff: ({ context }) => context.landingKind === 'projected_kickoff', - startsAtFrontierReply: ({ context }) => context.landingKind === 'frontier_turn', - startsAtVisibleGeneration: ({ context }) => context.landingKind === 'visible_generation', - startsAtProjectedRecovery: ({ context }) => context.landingKind === 'projected_recovery', - }, -}).createMachine({ - id: 'phase', - context: ({ input }) => ({ - landingKind: input.landing.kind, - phaseKey: input.landing.phaseKey, - frontierTurnId: input.landing.kind === 'frontier_turn' ? input.landing.turnId : null, - frontierTurnKind: input.landing.kind === 'frontier_turn' ? input.landing.turnKind : null, - lastAnsweredTurnId: - input.landing.kind === 'visible_generation' ? input.landing.answeredTurnId : null, - pendingSuccessorKind: - input.landing.kind === 'visible_generation' ? input.landing.successorKind : null, - generationFailure: - input.landing.kind === 'projected_recovery' ? input.landing.reason : null, - }), - initial: 'bootstrapping', - states: { - // Hydration does not always land in kickoff. This transient entry state narrows - // the open-phase landing into the truthful visible bottom artifact. - bootstrapping: { - always: [ - { guard: 'startsAtProjectedKickoff', target: 'awaiting_kickoff' }, - { guard: 'startsAtFrontierReply', target: 'active.awaiting_reply' }, - { guard: 'startsAtVisibleGeneration', target: 'active.generating_successor' }, - { guard: 'startsAtProjectedRecovery', target: 'active.awaiting_recovery' }, - ], - }, - - // Kickoff is now a projected control card, not a durable turn row. Accepting - // it initiates first-successor generation; there is no kickoff turn to answer. - awaiting_kickoff: { - on: { - KICKOFF_ACCEPTED: { - target: 'active.interviewer_processing', - actions: assign({ - frontierTurnId: null, - frontierTurnKind: null, - lastAnsweredTurnId: null, - pendingSuccessorKind: null, - generationFailure: null, - }), - }, - FORCE_CLOSE_REQUESTED: 'closed_via_force', - }, - }, - - // Open phase. Exactly one visible bottom artifact exists at all times here: - // a frontier turn, visible generation state, or projected recovery control. - active: { - on: { - FORCE_CLOSE_REQUESTED: 'closed_via_force', - }, - initial: 'awaiting_reply', - states: { - awaiting_reply: { - on: { - // Closure rejection stays on the normal reply path. A rejected closure - // proposal is still just a structured reply to the current frontier. - REPLY_SUBMITTED: { - target: 'interviewer_processing', - actions: [ - assign(({ event }) => ({ lastAnsweredTurnId: event.turnId })), - { - type: 'emitTurnAnswered', - params: ({ event }) => ({ turnId: event.turnId }), - }, - ], - }, - }, - }, - - // Interviewer agent is deciding what comes next. Must resolve to exactly - // one of: generative successor, closure proposal, accept close, or - // projected recovery. No silent exits. - interviewer_processing: { - on: { - INTERVIEWER_DECIDED: [ - { guard: 'isAcceptClose', target: '#phase.closed_via_interviewer' }, - { - guard: 'isClosureProposal', - target: 'generating_successor', - actions: assign({ pendingSuccessorKind: 'closure_proposal' }), - }, - { - guard: 'isGenerativeSuccessor', - target: 'generating_successor', - actions: assign(({ event }) => ({ - pendingSuccessorKind: - event.type === 'INTERVIEWER_DECIDED' ? event.successorKind : null, - })), - }, - ], - GENERATION_FAILED: { - target: 'awaiting_recovery', - actions: assign(({ event }) => ({ - frontierTurnId: null, - frontierTurnKind: null, - pendingSuccessorKind: null, - generationFailure: event.type === 'GENERATION_FAILED' ? event.reason : null, - })), - }, - }, - }, - - // The runtime host must only emit `SUCCESSOR_GENERATED` after the durable - // turn exists. This chart treats visible generation as a truthful open-phase - // bottom artifact, including hydration into that state when durable evidence - // justifies it. - generating_successor: { - on: { - SUCCESSOR_GENERATED: { - target: 'awaiting_reply', - actions: assign(({ event }) => ({ - frontierTurnId: event.type === 'SUCCESSOR_GENERATED' ? event.turnId : null, - frontierTurnKind: event.type === 'SUCCESSOR_GENERATED' ? event.turnKind : null, - pendingSuccessorKind: null, - generationFailure: null, - })), - }, - GENERATION_FAILED: { - target: 'awaiting_recovery', - actions: assign(({ event }) => ({ - frontierTurnId: null, - frontierTurnKind: null, - pendingSuccessorKind: null, - generationFailure: event.type === 'GENERATION_FAILED' ? event.reason : null, - })), - }, - }, - }, - - // Recovery is a projected control, not a durable turn row. Exiting recovery - // re-enters the normal successor path and must ultimately produce an - // ordinary durable frontier turn. - awaiting_recovery: { - on: { - RECOVERY_CONTINUED: { - target: 'interviewer_processing', - actions: assign({ - frontierTurnId: null, - frontierTurnKind: null, - pendingSuccessorKind: null, - generationFailure: null, - }), - }, - }, - }, - }, - }, - - // Terminal states. Output carries the closure basis up to the parent spec - // chart via `onDone`; no durable writes happen here. - closed_via_interviewer: { - type: 'final', - }, - closed_via_force: { - type: 'final', - }, - }, - output: ({ event }) => - event.type === 'xstate.done.state.phase.closed_via_force' - ? { basis: 'force' } - : { basis: 'interviewer' }, -}); diff --git a/archive/docs/design/state-machines/spec-machine.ts b/archive/docs/design/state-machines/spec-machine.ts deleted file mode 100644 index e11c04f59..000000000 --- a/archive/docs/design/state-machines/spec-machine.ts +++ /dev/null @@ -1,269 +0,0 @@ -// Spec-level state machine for Brunch. -// Paste-ready for Stately Studio (studio.stately.ai) — XState v5. -// -// One instance per specification chart. This is intentionally slimmer than the -// older draft: it owns cross-phase legality, invokes at most one phase actor, -// and retries the authoritative phase-outcome write. A runtime host around this -// chart owns hydration reconciliation, observer backlog reseeding, leases, -// cancellation, stale-event rejection, and write-ordering discipline. - -import { assign, fromPromise, setup } from 'xstate'; -import { phaseMachine } from './phase-machine'; - -type TurnId = string; - -type PhaseKey = 'grounding' | 'design' | 'requirements' | 'criteria'; - -type DurableFrontierTurnKind = 'question' | 'grounding' | 'review' | 'closure'; - -type SuccessorKind = 'question' | 'grounding' | 'review' | 'closure_proposal' | 'accept_close'; - -type RecoveryReason = 'generation_failed' | 'frontier_missing' | 'frontier_invalid'; - -type ClosureBasis = 'interviewer' | 'force'; - -type SpecificationLanding = - | { kind: 'projected_kickoff'; phaseKey: PhaseKey } - | { - kind: 'frontier_turn'; - phaseKey: PhaseKey; - turnId: TurnId; - turnKind: DurableFrontierTurnKind; - } - | { - kind: 'visible_generation'; - phaseKey: PhaseKey; - answeredTurnId: TurnId; - successorKind: SuccessorKind | null; - } - | { kind: 'projected_recovery'; phaseKey: PhaseKey; reason: RecoveryReason } - | { kind: 'handoff'; closedPhaseKey: PhaseKey; nextPhaseKey: PhaseKey | null } - | { kind: 'complete' }; - -type OpenPhaseLanding = Extract< - SpecificationLanding, - { kind: 'projected_kickoff' | 'frontier_turn' | 'visible_generation' | 'projected_recovery' } ->; - -type Context = { - currentPhaseKey: PhaseKey | null; - landing: SpecificationLanding | null; - pendingClosureBasis: ClosureBasis | null; - boundaryWriteFailure: string | null; -}; - -type Event = - | { - type: 'SPEC_HYDRATED'; - landing: SpecificationLanding; - pendingCaptureTurnIds: TurnId[]; - } - | { type: 'TURN_ANSWERED'; turnId: TurnId } - | { type: 'CAPTURE_SUCCEEDED'; turnId: TurnId } - | { type: 'CAPTURE_FAILED'; turnId: TurnId; error: string } - | { type: 'BOUNDARY_RETRY_REQUESTED' }; - -export const specMachine = setup({ - types: { - context: {} as Context, - events: {} as Event, - input: {} as { specId: string }, - }, - actors: { - phaseMachine, - // Writes the authoritative phase outcome (summary + closure basis) for the - // current phase. Failure lands the chart in `boundary_write_failed`. - recordPhaseOutcome: fromPromise( - async ({ input: _input }) => { - // await api.recordPhaseOutcome({ phaseKey, basis }) - throw new Error('not implemented'); - }, - ), - }, - actions: { - // The runtime host owns the durable-backed capture backlog. These actions are - // still useful chart-level facts for documentation, but the queue itself lives - // outside this chart. - enqueueObserverCapture: (_, _params: { turnId: TurnId }) => { - // runtime.enqueueCapture(params.turnId) - }, - markCaptureSucceeded: (_, _params: { turnId: TurnId }) => { - // runtime.markCaptureSucceeded(params.turnId) - }, - markCaptureFailed: (_, _params: { turnId: TurnId; error: string }) => { - // runtime.markCaptureFailed(params.turnId, params.error) - }, - reseedCaptureBacklog: (_, _params: { turnIds: TurnId[] }) => { - // runtime.reseedCaptureBacklog(params.turnIds) - }, - requestReconciliation: () => { - // runtime.reconcileAndSendHydrated() - }, - }, - guards: { - isOpenPhaseLanding: ({ event }) => - event.type === 'SPEC_HYDRATED' && - (event.landing.kind === 'projected_kickoff' || - event.landing.kind === 'frontier_turn' || - event.landing.kind === 'visible_generation' || - event.landing.kind === 'projected_recovery'), - isCompleteLanding: ({ event }) => event.type === 'SPEC_HYDRATED' && event.landing.kind === 'complete', - isHandoffLanding: ({ event }) => event.type === 'SPEC_HYDRATED' && event.landing.kind === 'handoff', - }, -}).createMachine({ - id: 'spec', - context: { - currentPhaseKey: null, - landing: null, - pendingClosureBasis: null, - boundaryWriteFailure: null, - }, - initial: 'loading', - // Turn-answered and capture-settled events can arrive in any state. The runtime - // host may continue draining capture backlog while the spec chart is waiting on - // reconciliation, a retry, or has already moved on from the just-answered phase. - on: { - TURN_ANSWERED: { - actions: { - type: 'enqueueObserverCapture', - params: ({ event }) => ({ turnId: event.turnId }), - }, - }, - CAPTURE_SUCCEEDED: { - actions: { - type: 'markCaptureSucceeded', - params: ({ event }) => ({ turnId: event.turnId }), - }, - }, - CAPTURE_FAILED: { - actions: { - type: 'markCaptureFailed', - params: ({ event }) => ({ turnId: event.turnId, error: event.error }), - }, - }, - }, - states: { - // Waiting for durable truth to be reconciled into a landing union. The runtime - // host computes the landing before dispatching `SPEC_HYDRATED`. - loading: { - on: { - SPEC_HYDRATED: [ - { - guard: 'isCompleteLanding', - target: 'complete', - actions: [ - assign(({ event }) => ({ - landing: event.type === 'SPEC_HYDRATED' ? event.landing : null, - currentPhaseKey: null, - pendingClosureBasis: null, - boundaryWriteFailure: null, - })), - { - type: 'reseedCaptureBacklog', - params: ({ event }) => ({ turnIds: event.pendingCaptureTurnIds }), - }, - ], - }, - { - guard: 'isHandoffLanding', - actions: [ - assign(({ event }) => ({ - landing: event.type === 'SPEC_HYDRATED' ? event.landing : null, - currentPhaseKey: null, - pendingClosureBasis: null, - boundaryWriteFailure: null, - })), - { - type: 'reseedCaptureBacklog', - params: ({ event }) => ({ turnIds: event.pendingCaptureTurnIds }), - }, - ], - }, - { - guard: 'isOpenPhaseLanding', - target: 'phase_running', - actions: [ - assign(({ event }) => ({ - landing: event.type === 'SPEC_HYDRATED' ? event.landing : null, - currentPhaseKey: - event.type === 'SPEC_HYDRATED' && - (event.landing.kind === 'projected_kickoff' || - event.landing.kind === 'frontier_turn' || - event.landing.kind === 'visible_generation' || - event.landing.kind === 'projected_recovery') - ? event.landing.phaseKey - : null, - pendingClosureBasis: null, - boundaryWriteFailure: null, - })), - { - type: 'reseedCaptureBacklog', - params: ({ event }) => ({ turnIds: event.pendingCaptureTurnIds }), - }, - ], - }, - ], - }, - }, - - // Exactly one phase actor is invoked here. The child owns open-phase legality; - // this chart reacts only to closure and records the durable phase outcome. - phase_running: { - invoke: { - id: 'phase', - src: 'phaseMachine', - input: ({ context }) => ({ landing: context.landing as OpenPhaseLanding }), - onDone: { - target: 'recording_phase_outcome', - actions: assign(({ event }) => ({ - pendingClosureBasis: event.output.basis, - })), - }, - }, - }, - - // Durable phase-boundary write: record the authoritative outcome for the phase - // that just closed. The runtime host will reconcile that durable truth into the - // next landing, which may be handoff, projected kickoff, another open landing, - // or final completion. - recording_phase_outcome: { - invoke: { - src: 'recordPhaseOutcome', - input: ({ context }) => ({ - phaseKey: context.currentPhaseKey!, - basis: context.pendingClosureBasis!, - }), - onDone: { - target: 'loading', - actions: [ - assign({ - currentPhaseKey: null, - landing: null, - pendingClosureBasis: null, - boundaryWriteFailure: null, - }), - { type: 'requestReconciliation' }, - ], - }, - onError: { - target: 'boundary_write_failed', - actions: assign(({ event }) => ({ - boundaryWriteFailure: - event.error instanceof Error ? event.error.message : 'phase outcome recording failed', - })), - }, - }, - }, - - // Shared retry state for the authoritative phase-outcome write. - boundary_write_failed: { - on: { - BOUNDARY_RETRY_REQUESTED: 'recording_phase_outcome', - }, - }, - - complete: { - type: 'final', - }, - }, -}); diff --git a/archive/docs/next/architecture/jsonl-session-viability-note.md b/archive/docs/next/architecture/jsonl-session-viability-note.md deleted file mode 100644 index 6959116bd..000000000 --- a/archive/docs/next/architecture/jsonl-session-viability-note.md +++ /dev/null @@ -1,237 +0,0 @@ -# JSONL Session Viability Spike - -## Question - -Can `pi` JSONL sessions serve as the durable transcript authority for the Brunch `next` POC while still carrying the extra structured turn and continuity data Brunch wants to store? - -The concrete target comes from [the POC PRD](./brunch-poc-architecture-prd.md): preserve raw assistant and user payloads, preserve structured turn artifacts and custom per-turn data on both sides, and preserve continuity metadata such as `lastSeenLsn`, interest sets, and compaction anchors. - -## Approach - -- Compare the POC transcript requirements in [the PRD](./brunch-poc-architecture-prd.md) against the actual `SessionEntry` shapes in `~/Clones/earendil-works/pi/packages/coding-agent/src/core/session-manager.ts`. -- Check how `buildSessionContext()`, `convertToLlm()`, and compaction treat each entry kind. -- Treat [the later session-format investigation](./artifacts/session-re-extending-sessions.jsonl) as corrective authority where the earlier [architecture transcript](./artifacts/transcript-of-pi-architecture-review.md) speculated beyond what current pi actually exposes. - -## Verdict - -Yes, conditionally. - -`pi` JSONL is viable as the transcript authority for the POC if Brunch adopts one strong rule: - -- raw transcript payloads stay in native `message` entries -- Brunch-specific hidden state lives in `custom` entries -- Brunch-specific model-visible injected state lives in `custom_message` entries - -That is enough for the POC's session goals. - -It is not enough if Brunch needs any of the following to be true in M2: - -- a pluggable session storage backend behind `SessionManager` -- arbitrary new top-level session entry kinds without touching pi core -- arbitrary extra fields embedded directly into native `user` or `assistant` messages -- a DB-backed canonical transcript store that pi can use directly without projection - -## Corrections To Earlier Speculation - -These earlier ideas should be treated as superseded by the later session-format investigation and the current pi source: - -- A new top-level session entry kind such as `interest_set` is not currently a public extension seam. Under current pi, Brunch should encode this state in `custom` entries rather than inventing a new entry `type`. -- `display: false` on `custom_message` does not mean "model-hidden". It only affects TUI rendering. `convertToLlm()` still projects the content into a user-role message for the LLM. -- `SessionManager` is not a supported storage adapter seam. It is both the session model and the filesystem persistence implementation. - -## Requirement Mapping - -| Brunch need | Current pi shape | Verdict | Notes | -| --- | --- | --- | --- | -| Project-local transcript directory | `SessionManager.create(cwd, sessionDir)` | Yes | Brunch can point this at `.brunch/sessions/`. | -| Session identity and lineage | `SessionHeader.id`, `cwd`, `parentSession` | Yes | Enough for project-local session identity and fork lineage. | -| Raw user payloads | `message` entry with `message.role === "user"` | Yes | Native `UserMessage` stores text or text/image blocks. | -| Raw assistant payloads | `message` entry with `message.role === "assistant"` | Yes | Native `AssistantMessage` keeps text, thinking blocks, tool calls, provider, model, usage, stop reason, optional `errorMessage`, and optional `responseId`. | -| Raw tool results | `message` entry with `message.role === "toolResult"` | Yes | This is the one native message type that already has a general `details` slot. | -| Mid-session model changes | `model_change` | Yes | Native entry, already replayed by `buildSessionContext()`. | -| Mid-session thinking-level changes | `thinking_level_change` | Yes | Native entry, already replayed by `buildSessionContext()`. | -| Session title | `session_info` | Yes | Only supports `name`, not arbitrary metadata. | -| User bookmarks / checkpoints | `label` | Partial | Good for human-facing markers, not a general metadata channel. | -| Branching inside one session file | Tree via `id` / `parentId` plus `branch_summary` | Yes | Native fit for branch-aware transcript history. | -| Extracting one branch to a new session file | `createBranchedSession()` | Yes | Keeps path entries and labels. | -| Compaction summaries | `compaction` | Yes | Native fit; `details` gives one structured side channel. | -| Hidden continuity state (`lastSeenLsn`, interest sets, compaction anchors, UI-only metadata) | `custom` | Yes | Best place for Brunch-owned hidden state. `custom` is ignored by `buildSessionContext()`. | -| Model-visible continuity injection (`worldUpdate`, graph snapshot reminders, review artifacts) | `custom_message` | Yes | Best place for Brunch-owned model-visible injected state. `details` stays local; `content` goes to the model. | -| Structured turn metadata for a whole turn | `custom` sidecar written at `turn_end` | Yes | Good POC fit. Append a Brunch turn snapshot after the turn is fully persisted. | -| Structured metadata attached to a specific native `user` or `assistant` message | Sidecar `custom` entry by convention | Partial | Feasible, but native user/assistant messages do not have their own metadata slot. Brunch must maintain the attachment convention itself. | -| Custom session schema as pi's primary storage shape | None | No | Current pi expects the built-in JSONL entry union. | -| Database as pi's primary transcript store | None | No | Requires projection, mirroring, or a pi core change. | - -## What Fits Cleanly - -### 1. Raw transcript preservation - -This is the strongest part of the fit. - -- Native `message` entries already preserve the raw user prompt shape. -- Native assistant messages already preserve more than just final text: they carry thinking blocks, tool calls, usage, provider/model identity, stop reason, and optional response identifiers. -- Native tool results already preserve structured `details`, which gives Brunch one built-in place for tool-scoped metadata without inventing its own sidecar. - -For replay, export, and session resume, this is already the shape pi understands. - -### 2. Hidden Brunch session state - -`custom` entries are the right place for Brunch-owned state that should survive reload but should not enter model context. - -This covers the POC's continuity metadata directly: - -- `lastSeenLsn` -- interest-set snapshots -- compaction anchors -- UI-only annotations -- any Brunch bookkeeping needed to reconstruct per-turn state on reload - -Because `custom` entries are append-only, Brunch should treat them as a log where the latest snapshot for a given `customType` wins. - -### 3. Model-visible injected state - -`custom_message` entries are the right place for between-turn messages such as `worldUpdate`. - -This matches the corrected transcript architecture well: - -- `prepareNextTurn` decides whether relevant graph changes occurred -- Brunch creates a `custom_message` -- `convertToLlm()` projects it into a normal user-role message for the next model call - -This is a natural fit for continuity repair, graph snapshot reminders, and other Brunch-authored synthetic context. - -## Sharp Constraints - -### `custom_message` is not hidden from the model - -This is the easiest mistake to make. - -`custom_message.display` only affects TUI rendering. The content still goes through `createCustomMessage()` and `convertToLlm()`, which means it becomes a user-role message in the next LLM payload. - -Rule: if the model must not see it, use `custom`, not `custom_message`. - -### Native `user` and `assistant` messages do not have a freeform metadata field - -`toolResult` has `details`. `user` and `assistant` do not. - -So Brunch-specific per-message metadata for those roles must live in an adjacent sidecar entry, not inside the native message object. - -That is acceptable for the POC, but it means Brunch needs a convention for attaching sidecars to raw message entries. - -### `message_end` is not the cleanest place to attach sidecars - -Inside `AgentSession`, extension and listener events are emitted before the `SessionManager.appendMessage(event.message)` persistence step for `message_end`. - -That means the easiest reliable seam for Brunch-owned transcript sidecars is usually `turn_end`, when the raw turn messages have already been persisted. - -Implication: - -- turn-level sidecars are easy in the POC -- precise per-message sidecars are still possible, but Brunch may need a slightly more deliberate wrapper or a small pi seam if exact entry IDs become important - -### There is no supported `SessionStore` abstraction - -The later session investigation was right to call this out. `SessionManager` is not just a repository interface. It owns filesystem persistence directly. - -Implication: - -- JSONL-first is viable for M2 -- JSONL-as-canonical with DB projection is viable later -- DB-as-primary-through-pi is not a native path today - -## Recommended POC Conventions - -### Use one Brunch namespace per concern - -Suggested `customType` values: - -- `brunch/session_state/v1` -- `brunch/turn_meta/v1` -- `brunch/world_update/v1` -- `brunch/compaction_anchor/v1` - -### Prefer turn-level sidecars over per-message sidecars in M2 - -For the first proof, store one structured Brunch snapshot at `turn_end` instead of trying to decorate every native message independently. - -That keeps the POC simple while still allowing: - -- raw native messages for faithful transcript replay -- structured Brunch-owned turn metadata for reload -- later escalation to finer-grained attachment if needed - -### Keep graph truth out of JSONL - -Use JSONL only for transcript truth and transcript-adjacent continuity state. - -The graph remains in SQLite. JSONL should not become a shadow graph database. - -## Suggested Entry Shapes - -```json -{ - "type": "custom", - "customType": "brunch/session_state/v1", - "data": { - "lastSeenLsn": 481, - "interestSet": { - "direct": ["intent:123", "design:456"], - "mentioned": ["intent:789"] - }, - "compactionAnchor": { - "summaryEntryId": "f6g7h8i9" - } - } -} -``` - -```json -{ - "type": "custom", - "customType": "brunch/turn_meta/v1", - "data": { - "turnIndex": 12, - "kind": "assistant_turn", - "artifacts": { - "contextPackIds": ["cp_12"], - "graphRefs": ["intent:123", "design:456"] - } - } -} -``` - -```json -{ - "type": "custom_message", - "customType": "brunch/world_update/v1", - "content": "Since your last turn, 2 relevant graph items changed and coherence is now degraded.", - "display": true, - "details": { - "lastSeenLsn": 481, - "currentLsn": 493, - "changedRefs": ["intent:123", "design:456"], - "coherence": "degraded" - } -} -``` - -## M2 Proof Checklist - -If Brunch wants to validate this direction before implementation spreads, M2 should prove these exact cases: - -1. Raw user, assistant, and tool-result payloads survive save, reload, and resume with no information loss that matters to replay. -2. A `brunch/session_state/v1` `custom` entry survives reload and can restore `lastSeenLsn` and interest-set snapshots. -3. A `brunch/world_update/v1` `custom_message` can be injected through the next-turn seam and reliably reaches the model as intended. -4. A `brunch/turn_meta/v1` sidecar survives branch, compaction, and resume well enough for POC turn reconstruction. -5. The POC can decide whether turn-level sidecars are enough, or whether exact per-message sidecars justify a small pi seam. - -## Recommendation - -Proceed with the POC on a JSONL-first basis. - -The burden of proof is no longer "can pi store transcripts at all?" It clearly can. The real M2 question is narrower: - -- can Brunch live with sidecar conventions around native messages -- and is turn-level structured metadata enough for the first proof - -If the answer to either becomes no, the next move should be a canonical richer transcript substrate with pi JSONL projection, not trying to stretch `SessionManager` into a generic storage backend. diff --git a/archive/docs/next/architecture/plan-graph-petri-orchestration.md b/archive/docs/next/architecture/plan-graph-petri-orchestration.md deleted file mode 100644 index da19b4e4e..000000000 --- a/archive/docs/next/architecture/plan-graph-petri-orchestration.md +++ /dev/null @@ -1,485 +0,0 @@ -# Plan-Graph Petri Orchestration - -> Status: speculative design note. -> Date: 2026-05-18. -> Inputs: `docs/design/SPEC_WORKSPACE_GRAPHS.md` and the Petri-net basic orchestration demo. -> Purpose: explore how a Brunch `plan-graph` can hand off executable work to a Petri-net-based orchestrator without collapsing semantic completion into mechanical completion. - -## 1. Thesis - -A Brunch `plan-graph` should remain the semantic source of planning truth. A Petri net should be an executable/read-model projection of one active plan subtree: usually a frontier, slice, or small slice cluster. - -```text -intent/oracle/design/plan graphs + relation policy - -> compile active plan subtree - -> typed Petri execution net - -> execution events, evidence refs, status suggestions - -> Brunch graph reconciliation / explicit status declaration -``` - -The Petri net is valuable because it makes execution state, concurrency, resource constraints, retries, blocking, and terminal markings explicit. The Brunch graphs are valuable because they preserve why the work matters and what evidence counts as completion. - -The critical design pressure is this: - -> A token reaching `Done` is not the same thing as the plan claim being semantically established. - -Therefore speculative nets should model both mechanical execution and semantic completion. - -## 2. Layer split - -### 2.1 Mechanical layer - -Mechanical execution answers: what has been dispatched, produced, run, verified, blocked, or retried? - -Example mechanical places: - -```text -PlanSliceSelected -ContextPackReady -ImplementationInProgress -CodeArtifactReady -TestArtifactReady -TestReportReady -VerifyPassed -BranchReady -TaskBlocked -TaskAbandoned -``` - -Example mechanical transitions: - -```text -BuildContextPack -DispatchImplementation -ImplementationDone -RunInnerLoop -RunGateVerify -OpenPullRequest -RetryFix -AbandonAfterMaxRetries -``` - -### 2.2 Semantic layer - -Semantic completion answers: has this evidence established the intended plan claim against current intent/design/oracle state? - -Example semantic places: - -```text -GraphRevisionCurrent -IntentTargetsCurrent -OracleSatisfied -DesignIntroduced -DesignExercised -RiskRetired -RiskAccepted -CompletionClaimReviewed -CompletionClaimAccepted -PlanDoneAccepted -``` - -Example semantic transitions: - -```text -AssessOracleSatisfaction -AssessDesignExercised -AssessIntentEstablished -ReviewResidualRisk -ReviewCompletionClaim -DeclarePlanDone -MarkSemanticReviewNeeded -``` - -Mechanical transitions produce candidate evidence. Semantic transitions judge whether that evidence satisfies the graph-derived requirements. - -```text -RunTests -> TestReportReady -AssessOracleSatisfaction -> OracleSatisfied -``` - -A test report is not itself oracle satisfaction. It becomes oracle satisfaction only if it maps to the required oracle node, was run against the right artifact and graph revision, and satisfies the relation-policy-derived gate. - -## 3. Token taxonomy - -### 3.1 Resource tokens - -Resource tokens model scarce or permissioned executors. - -```text -CodingAgentAvailable(agentId) -ReviewAgentAvailable(agentId) -HumanReviewerAvailable(userId) -BrowserHarnessAvailable(harnessId) -GraphWriteCapabilityAvailable(scope) -CIRunnerAvailable(runnerId) -``` - -These prevent the net from pretending that every transition can always run. - -### 3.2 Context and revision tokens - -```text -ContextPackReady(planNodeId, contextPackRef, graphRevision) -GraphRevisionCurrent(graphRevision) -GraphRevisionStale(graphRevision) -SemanticReviewNeeded(planNodeId, staleRevision, currentRevision) -``` - -Every meaningful evidence-producing token should carry the graph revision it was derived from. Semantic transitions require current revision, or else route to reconciliation. - -### 3.3 Artifact and evidence tokens - -```text -CodeArtifactReady(planNodeId, artifactRef, graphRevision) -TestReportReady(planNodeId, reportRef, status, graphRevision) -OracleEvidenceReady(oracleNodeId, evidenceRef, graphRevision) -DesignEvidenceReady(designNodeId, evidenceRef, graphRevision) -PlanEvidenceBundleReady(planNodeId, evidenceRefs, graphRevision) -``` - -Raw logs, screenshots, transcripts, and test reports should live in an artifact/evidence store. Tokens carry stable refs. - -### 3.4 Semantic satisfaction tokens - -```text -OracleSatisfied(planNodeId, oracleNodeId, evidenceRef) -DesignIntroduced(planNodeId, designNodeId, evidenceRef) -DesignExercised(planNodeId, designNodeId, evidenceRef) -IntentEstablished(planNodeId, intentNodeId, evidenceRefs) -RiskRetired(planNodeId, riskRef, evidenceRef) -RiskAccepted(planNodeId, riskRef, acceptedBy, rationaleRef) -CompletionClaimAccepted(planNodeId, acceptedBy, rationaleRef) -PlanDoneAccepted(planNodeId) -``` - -These tokens are judgment-bearing. Some can be produced by deterministic checks; others may require review agents or humans. - -## 4. Canonical slice-net template - -A speculative Brunch slice subnet can be organized into four coupled lanes. - -```text -Mechanical lane: - SliceSelected - -> ContextPackReady - -> ImplementationInProgress - -> CodeArtifactReady - -> TestReportReady - -> VerifyPassed - -Oracle lane: - RequiredOracleKnown - -> OracleEvidenceReady - -> OracleSatisfied - -Design lane: - RequiredDesignKnown - -> DesignEvidenceReady - -> DesignIntroduced / DesignExercised - -Semantic lane: - IntentTargetsCurrent - -> IntentEstablished - -> CompletionClaimAccepted - -> PlanDoneAccepted -``` - -The terminal transition joins the lanes. - -```text -DeclarePlanDone consumes: - VerifyPassed(planNodeId) - IntentEstablished(planNodeId, ...) - OracleSatisfied(planNodeId, requiredOracleIds...) - DesignIntroduced/DesignExercised(planNodeId, requiredDesignIds...) - RiskRetired or RiskAccepted for required risks - GraphRevisionCurrent(graphRevision) - CompletionClaimAccepted(planNodeId) - -DeclarePlanDone produces: - PlanDoneAccepted(planNodeId) - StatusProjectionSuggested(planNodeId, targetRefs) -``` - -Near-term, `PlanDoneAccepted` should suggest or support a declared plan status change. It should not silently become canonical truth unless the workflow explicitly grants that authority. - -## 5. Relation-policy compilation - -The workspace graph relation policy can compile semantic edges into Petri-net requirements. - -| Relation kind | Possible Petri-net requirement | -|---|---| -| `plan.depends_on` | prerequisite token or guard before `SliceSelected` / `BuildContextPack` | -| `plan.establishes_intent` | require `IntentEstablished(planNodeId, intentNodeId)` before done | -| `plan.introduces_design` | require `DesignIntroduced(planNodeId, designNodeId)` before done | -| `plan.exercises_design` | require `DesignExercised(planNodeId, designNodeId)` before done | -| `plan.implements_oracle` | require oracle implementation/evidence-producing branch | -| `plan.verified_by_oracle` | require `OracleSatisfied(planNodeId, oracleNodeId)` before done | -| `plan.retires_risk` | require `RiskRetired` or explicit accepted deferral | - -This makes Petri simulation a planning oracle: if relation policy says an edge is gating, then `PlanDoneAccepted` should be unreachable unless the corresponding semantic token can be produced. - -## 6. Transition contracts - -The Petri structure alone is not enough. Each transition needs a typed execution contract. - -```ts -type TransitionContract = { - transitionId: string - kind: "mechanical" | "semantic" | "review" | "status_projection" - actor?: "coding_agent" | "review_agent" | "human" | "tool" | "orchestrator" - consumes: Array - produces: Array - guard?: GuardSpec - action?: CapabilityBinding - idempotencyKey?: string - timeout?: string - retryPolicy?: RetryPolicy - cancellationPolicy?: CancellationPolicy - emits: Array -} -``` - -Important contract fields: - -- **guard** — checks graph revision, required relation-policy gates, artifact status, resource availability. -- **action** — binds transition to a Brunch capability, shell command, agent task, browser/manual harness, or no-op semantic assessment. -- **idempotency key** — prevents duplicate graph writes, duplicate PR creation, duplicate test dispatch, etc. -- **events** — durable replayable records for Brunch reconciliation. - -## 7. Event model - -The orchestrator should emit structured events rather than only final markings. - -```text -transition_enabled -transition_fired -task_dispatched -task_completed -artifact_produced -oracle_passed -oracle_failed -design_exercised -graph_revision_stale -semantic_review_requested -completion_claim_accepted -status_projection_suggested -status_declared -transition_blocked -net_deadlocked -``` - -These events support audit, resumption, visualization, and graph reconciliation. - -## 8. Failure-mode nets - -### 8.1 Stale graph during execution - -Scenario: - -```text -ContextPackReady(P1, G42) -ImplementationInProgress(P1, G42) -Graph advances to G43 -CodeArtifactReady(P1, G42) -``` - -Transition: - -```text -DetectStaleGraph consumes: - CodeArtifactReady(P1, G42) - GraphRevisionCurrent(G43) - -guard: - G42 != G43 - -produces: - GraphRevisionStale(G42) - SemanticReviewNeeded(P1, G42, G43) -``` - -Completion remains blocked until either: - -```text -ReconcileArtifactsToCurrentGraph -> EvidenceReady(P1, G43) -``` - -or: - -```text -RebuildContextPack -> rerun mechanical branch against G43 -``` - -### 8.2 Missing oracle - -Scenario: - -```text -CodeArtifactReady -VerifyPassed -RequiredOracleKnown(O7) -O7 implementation status: not_implemented -``` - -Net behavior: - -```text -AssessOracleSatisfaction is not enabled. -ImplementRequiredOracle branch becomes enabled. -``` - -The plan can reach mechanical verification but cannot reach semantic completion. - -```text -VerifyPassed != OracleSatisfied -``` - -### 8.3 Design not exercised - -Scenario: - -```text -Tests pass, but implementation bypasses intended seam D2. -``` - -Net behavior: - -```text -AssessDesignExercised consumes: - CodeArtifactReady - RequiredDesignKnown(D2) - DesignEvidenceReady? - -guard fails: - evidence does not show path crossing D2 - -produces: - DesignExerciseRejected(D2, reason) - ReworkRequired(P1) -``` - -This catches fake completion: a slice may pass tests while failing to exercise the architecture it was meant to establish. - -### 8.4 Residual risk accepted instead of retired - -Scenario: - -```text -KnownRisk(R3): concurrency race not fully tested in this slice. -``` - -Two possible branches: - -```text -RetireRiskMechanically -> RiskRetired(R3) -AcceptResidualRisk -> RiskAccepted(R3, acceptedBy, rationaleRef) -``` - -`DeclarePlanDone` can accept either only if relation policy allows accepted residual risk for that risk kind. Some risks may require retirement, not acceptance. - -## 9. Visualization model - -The UI should not force one graph to masquerade as the other. It should offer synchronized views. - -### 9.1 Semantic graph view - -Shows Brunch-native traceability: - -```text -P1 establishes R1 -P1 introduces D1 -P1 exercises D2 -P1 verified_by O1 -P1 depends_on P0 -``` - -Answers: - -- Why does this work matter? -- What completion evidence is required? -- What becomes stale if this node changes? -- Which semantic gates block completion? - -### 9.2 Petri execution view - -Shows runtime markings: - -```text -ContextPackReady -> ImplementationInProgress -> TestReportReady -> OracleSatisfied -> PlanDoneAccepted -``` - -Answers: - -- What can fire now? -- What is blocked? -- Which agents/tasks/artifacts are in flight? -- Which terminal states are reachable? -- Where did execution deadlock? - -### 9.3 Cross-highlighting - -- Click a plan slice: reveal its compiled Petri subnet. -- Click a Petri transition: show the relation-policy and graph edges that generated it. -- Click a blocked semantic place: show the missing oracle/design/intent/risk token. -- Click a `PlanDoneAccepted` token: show evidence lineage back to graph revision, artifacts, and semantic edges. - -## 10. Prototype candidates - -### Prototype A: happy-path tracer bullet - -Goal: compile one plan slice into a Petri subnet where mechanical and semantic lanes both complete. - -Acceptance criteria: - -- `PlanDoneAccepted` is reachable only after `VerifyPassed`, `OracleSatisfied`, `DesignExercised`, and `IntentEstablished`. -- Event log can explain how each semantic token was produced. - -### Prototype B: stale graph guard - -Goal: prove graph revision tokens prevent stale completion. - -Acceptance criteria: - -- An artifact produced from `G42` cannot satisfy semantic completion after current graph revision becomes `G43`. -- The net routes to reconciliation or context rebuild. - -### Prototype C: missing oracle branch - -Goal: prove the net distinguishes implementation completion from verification coverage. - -Acceptance criteria: - -- `VerifyPassed` can be reached while `PlanDoneAccepted` remains unreachable. -- `ImplementRequiredOracle` becomes enabled. - -### Prototype D: design bypass detection - -Goal: model a test-passing implementation that fails the intended architecture claim. - -Acceptance criteria: - -- `TestReportReady(status=passing)` does not imply `DesignExercised`. -- Rework is required unless a reviewer explicitly changes/accepts the design trace. - -## 11. Open questions - -1. How much semantic assessment can be deterministic versus agent/human-reviewed? -2. Should Petri-net compilation produce one flat net, hierarchical subnets, or colored nets with rich token payloads? -3. What is the minimal transition contract needed to avoid hidden imperative behavior in TypeScript kernels? -4. How should visual Petrinaut-compatible numeric token dimensions relate to execution-only typed token payloads? -5. Which relation kinds are completion-gating by default, and which are context-only? -6. When, if ever, may a Petri terminal marking directly declare `plan_node.executionStatus = done`? -7. How should aborted or abandoned runs affect plan/oracle/design status projections? -8. Can Petri simulation catch plan-shape defects before execution, such as unjoinable branches or unreachable semantic completion? - -## 12. Working conclusion - -Petri nets look most promising as a compiled execution semantics and visualization layer for active plan subtrees. The highest-leverage prototype is not a generic task runner. It is a slice net that makes this distinction executable: - -```text -mechanical completion produces evidence -semantic completion accepts evidence against graph-derived gates -``` - -If that distinction holds, Petri-net orchestration could become both an agent execution harness and a planning oracle for Brunch's workspace graphs. diff --git a/archive/docs/next/prototypes/plan-petri-visualization/README.md b/archive/docs/next/prototypes/plan-petri-visualization/README.md deleted file mode 100644 index 588dbedf6..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Plan-Graph Petri Visualization Prototype - -A disposable-but-trackable Brunch next prototype for pressure-testing `plan-graph` to Petri-net orchestration ideas. - -It uses plain HTML, TypeScript, Vite, and the local Cytoscape.js clone at: - -```text -~/Clones/cytoscape/cytoscape.js/dist/cytoscape.esm.mjs -``` - -## Run - -```bash -cd docs/next/prototypes/plan-petri-visualization -npm run dev -``` - -Then open the printed local URL. - -## What this tests - -The prototype renders one canonical slice-net template with scenario toggles: - -1. Happy path -2. Missing oracle -3. Design bypass -4. Stale graph -5. Risk pending - -The purpose is to see whether a Petri-net visualization can make this distinction concrete: - -```text -mechanical completion produces evidence -semantic completion accepts evidence against graph-derived gates -``` - -## Current limitations - -- Static final markings only; no animation or actual firing sequence yet. -- The Petri interpreter is intentionally tiny: enabled/blocked explanation by input-token presence. -- Semantic guards are represented as transition metadata, not executable predicates. -- The graph is hand-positioned rather than automatically laid out. diff --git a/archive/docs/next/prototypes/plan-petri-visualization/index.html b/archive/docs/next/prototypes/plan-petri-visualization/index.html deleted file mode 100644 index 9e5a2feb8..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - Plan-Graph Petri Orchestration Prototype - - -
-
-
-

Brunch next prototype

-

Plan-graph ⇄ Petri orchestration

-

- A small Cytoscape.js pressure-test for representing mechanical execution and semantic - completion as coupled Petri-net lanes. -

-
-
-
- -
- - -
-
- Mechanical - Oracle - Design - Semantic - Revision/risk - Token present - Token missing - Transition fired - Transition never fired -
-
-
- - -
-
- - - diff --git a/archive/docs/next/prototypes/plan-petri-visualization/package.json b/archive/docs/next/prototypes/plan-petri-visualization/package.json deleted file mode 100644 index f7e263b23..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 127.0.0.1", - "build": "vite build", - "preview": "vite preview --host 127.0.0.1" - } -} diff --git a/archive/docs/next/prototypes/plan-petri-visualization/src/cytoscape.d.ts b/archive/docs/next/prototypes/plan-petri-visualization/src/cytoscape.d.ts deleted file mode 100644 index fedd86f3d..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/src/cytoscape.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "/@fs/Users/lunelson/Clones/cytoscape/cytoscape.js/dist/cytoscape.esm.mjs" { - const cytoscape: any - export default cytoscape -} diff --git a/archive/docs/next/prototypes/plan-petri-visualization/src/main.ts b/archive/docs/next/prototypes/plan-petri-visualization/src/main.ts deleted file mode 100644 index ce6d7aba7..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/src/main.ts +++ /dev/null @@ -1,448 +0,0 @@ -import cytoscape from "/@fs/Users/lunelson/Clones/cytoscape/cytoscape.js/dist/cytoscape.esm.mjs" -import "./style.css" -import { - blockedTransitions, - completionStatus, - inputsFor, - outputsFor, - transitionStates, - type PetriNet, - type Scenario, -} from "./model" -import { scenarios, sliceNet } from "./scenarios" - -type CyElement = { - data: Record - position?: { x: number; y: number } - classes?: string -} - -const pickerEl = requireElement("scenario-picker") -const summaryEl = requireElement("scenario-summary") -const blockedEl = requireElement("blocked-list") -const selectionEl = requireElement("selection-detail") -const completionEl = requireElement("completion-detail") - -let selectedScenario = scenarios[0] - -const cy = cytoscape({ - container: requireElement("cy"), - elements: [], - layout: { name: "preset" }, - minZoom: 0.45, - maxZoom: 1.8, - wheelSensitivity: 0.2, - style: [ - { - selector: "node", - style: { - label: "data(label)", - "font-family": "Inter, ui-sans-serif, system-ui, sans-serif", - "font-size": 11, - "text-wrap": "wrap", - "text-max-width": 104, - "text-valign": "center", - "text-halign": "center", - color: "#000000", - width: 112, - height: 52, - "border-width": 2, - "border-color": "#c7d2fe", - "background-color": "#f8fafc", - }, - }, - { - selector: ":parent", - style: { - label: "data(label)", - shape: "round-rectangle", - "text-valign": "top", - "text-halign": "left", - "text-margin-x": 12, - "text-margin-y": 8, - "font-size": 13, - "font-weight": 800, - color: "#000000", - padding: 28, - "background-opacity": 0.12, - "border-width": 2, - "border-style": "dashed", - }, - }, - { - selector: "node.place", - style: { - shape: "ellipse", - width: 86, - height: 86, - "text-max-width": 72, - }, - }, - { - selector: "node.transition", - style: { - shape: "rectangle", - width: 86, - height: 46, - "background-color": "#111827", - color: "#ffffff", - "border-color": "#111827", - }, - }, - { - selector: "node.marked", - style: { - "background-color": "#16a34a", - "border-color": "#14532d", - "border-width": 5, - color: "#000000", - }, - }, - { - selector: "node.missing", - style: { - "background-color": "#ffffff", - "background-opacity": 0.42, - "border-color": "#94a3b8", - "border-style": "dashed", - "border-width": 2, - color: "#000000", - opacity: 0.72, - }, - }, - { - selector: "node.transition:not(.fired):not(.enabled):not(.blocked)", - style: { - "background-color": "#ffffff", - "background-opacity": 0.46, - "border-color": "#94a3b8", - "border-style": "dashed", - color: "#000000", - opacity: 0.72, - }, - }, - { - selector: "node.fired", - style: { - "background-color": "#4338ca", - "border-color": "#1e1b4b", - "border-width": 5, - color: "#000000", - }, - }, - { - selector: "node.enabled", - style: { - "background-color": "#15803d", - "border-color": "#14532d", - "border-width": 5, - color: "#000000", - }, - }, - { - selector: "node.blocked", - style: { - "background-color": "#dc2626", - "border-color": "#7f1d1d", - "border-width": 5, - color: "#000000", - }, - }, - { - selector: ".lane-mechanical", - style: { "border-color": "#60a5fa", "background-color": "#60a5fa" }, - }, - { - selector: ".lane-oracle", - style: { "border-color": "#a78bfa", "background-color": "#a78bfa" }, - }, - { - selector: ".lane-design", - style: { "border-color": "#f59e0b", "background-color": "#f59e0b" }, - }, - { - selector: ".lane-semantic", - style: { "border-color": "#10b981", "background-color": "#10b981" }, - }, - { - selector: ".lane-revision", - style: { "border-color": "#ef4444", "background-color": "#ef4444" }, - }, - { - selector: "edge", - style: { - width: 2, - "curve-style": "bezier", - "target-arrow-shape": "triangle", - "target-arrow-color": "#64748b", - "line-color": "#94a3b8", - label: "data(label)", - "font-size": 9, - color: "#64748b", - }, - }, - { - selector: ":selected", - style: { - "overlay-color": "#0f172a", - "overlay-opacity": 0.12, - "overlay-padding": 8, - }, - }, - ], -}) - -renderScenarioPicker() -render(selectedScenario) - -cy.on("tap", "node, edge", (event) => { - renderSelection(event.target.data()) -}) - -cy.on("tap", (event) => { - if (event.target === cy) { - selectionEl.className = "empty" - selectionEl.textContent = "Select a place, transition, or arc." - } -}) - -function renderScenarioPicker(): void { - pickerEl.innerHTML = "" - - for (const scenario of scenarios) { - const button = document.createElement("button") - button.type = "button" - button.textContent = scenario.label - button.className = scenario.id === selectedScenario.id ? "active" : "" - button.addEventListener("click", () => { - selectedScenario = scenario - renderScenarioPicker() - render(scenario) - }) - pickerEl.append(button) - } -} - -function render(scenario: Scenario): void { - cy.elements().remove() - cy.add(toCyElements(sliceNet, scenario)) - cy.fit(undefined, 42) - renderSummary(scenario) - renderBlocked(sliceNet, scenario) - renderCompletion(sliceNet, scenario) - selectionEl.className = "empty" - selectionEl.textContent = "Select a place, transition, or arc." -} - -function toCyElements(net: PetriNet, scenario: Scenario): CyElement[] { - const states = transitionStates(net, scenario) - const blockedIds = new Set( - states.filter((state) => !state.enabled && !state.fired).map((state) => state.id), - ) - const enabledIds = new Set(states.filter((state) => state.enabled).map((state) => state.id)) - const firedIds = scenario.firedTransitions - - const laneElements: CyElement[] = ["mechanical", "oracle", "design", "semantic", "revision"].map( - (lane) => ({ - data: { - id: `lane_${lane}`, - label: laneLabel(lane), - kind: "lane", - }, - classes: `lane-parent lane-${lane}`, - }), - ) - - const placeElements: CyElement[] = net.places.map((place) => { - const classes = ["place", `lane-${place.lane}`] - if (scenario.marking.has(place.id)) classes.push("marked") - else classes.push("missing") - - return { - data: { - ...place, - parent: `lane_${place.lane}`, - kind: "place", - tokenPresent: scenario.marking.has(place.id), - note: scenario.notesById[place.id], - }, - position: { x: place.x, y: place.y }, - classes: classes.join(" "), - } - }) - - const transitionElements: CyElement[] = net.transitions.map((transition) => { - const classes = ["transition", `lane-${transition.lane}`] - if (firedIds.has(transition.id)) classes.push("fired") - else if (enabledIds.has(transition.id)) classes.push("enabled") - else if (blockedIds.has(transition.id)) classes.push("blocked") - - const state = states.find((candidate) => candidate.id === transition.id) - - return { - data: { - ...transition, - parent: `lane_${transition.lane}`, - kind: "transition", - fired: firedIds.has(transition.id), - enabled: enabledIds.has(transition.id), - missingInputs: state?.missingInputs.map((place) => place.label) ?? [], - note: scenario.notesById[transition.id], - }, - position: { x: transition.x, y: transition.y }, - classes: classes.join(" "), - } - }) - - const arcElements: CyElement[] = net.arcs.map((arc) => ({ - data: { - ...arc, - kind: "arc", - }, - })) - - return [...laneElements, ...placeElements, ...transitionElements, ...arcElements] -} - -function renderSummary(scenario: Scenario): void { - summaryEl.innerHTML = ` -

${escapeHtml(scenario.headline)}

-

${escapeHtml(scenario.summary)}

-
Value probe: ${escapeHtml(scenario.valueProbe)}
- ` -} - -function renderBlocked(net: PetriNet, scenario: Scenario): void { - const blocked = blockedTransitions(net, scenario) - - if (blocked.length === 0) { - blockedEl.innerHTML = `

No blocked transitions in this final marking.

` - return - } - - blockedEl.innerHTML = blocked - .map((state) => { - const transition = net.transitions.find((candidate) => candidate.id === state.id) - if (!transition) return "" - const missing = state.missingInputs.map((place) => `
  • ${escapeHtml(place.label)}
  • `).join("") - const note = scenario.notesById[state.id] - ? `

    ${escapeHtml(scenario.notesById[state.id])}

    ` - : "" - - return ` -
    -

    ${escapeHtml(transition.label)}

    - ${note} -

    Missing inputs:

    -
      ${missing}
    -
    - ` - }) - .join("") -} - -function renderCompletion(net: PetriNet, scenario: Scenario): void { - const status = completionStatus(net, scenario) - - if (status.done) { - completionEl.innerHTML = ` -

    PlanDoneAccepted reached.

    -

    The terminal marking has all required semantic and mechanical tokens.

    - ` - return - } - - completionEl.innerHTML = ` -

    PlanDoneAccepted is not reachable from this marking.

    -

    Missing terminal inputs:

    -
      ${status.missing.map((place) => `
    • ${escapeHtml(place.label)}
    • `).join("")}
    - ` -} - -function renderSelection(data: Record): void { - const kind = String(data.kind) - - if (kind === "lane") { - selectionEl.className = "" - selectionEl.innerHTML = ` -

    ${escapeHtml(String(data.label))}

    -

    This compound node groups one orchestration lane. Unlike the earlier CSS background bands, it is part of the graph and zooms/pans with the Petri net.

    - ` - return - } - - if (kind === "arc") { - selectionEl.className = "" - selectionEl.innerHTML = ` -

    Arc

    -
    -
    Source
    ${escapeHtml(String(data.source))}
    -
    Target
    ${escapeHtml(String(data.target))}
    -
    - ` - return - } - - if (kind === "transition") { - const transitionId = String(data.id) - const transition = sliceNet.transitions.find((candidate) => candidate.id === transitionId) - const inputs = transition ? inputsFor(sliceNet, transition.id) : [] - const outputs = transition ? outputsFor(sliceNet, transition.id) : [] - selectionEl.className = "" - selectionEl.innerHTML = ` -

    ${escapeHtml(String(data.label))}

    -

    - ${data.fired ? "fired" : data.enabled ? "enabled" : "blocked"} -

    -

    ${escapeHtml(String(data.description ?? ""))}

    - ${data.guard ? `

    Guard: ${escapeHtml(String(data.guard))}

    ` : ""} - ${renderList("Compiled from", asStringArray(data.compiledFrom))} - ${renderList("Consumes", inputs.map((place) => place.label))} - ${renderList("Produces", outputs.map((place) => place.label))} - ${renderList("Missing", asStringArray(data.missingInputs))} - ${data.note ? `

    ${escapeHtml(String(data.note))}

    ` : ""} - ` - return - } - - selectionEl.className = "" - selectionEl.innerHTML = ` -

    ${escapeHtml(String(data.label))}

    -

    - ${data.tokenPresent ? "token present" : "token missing"} -

    -

    ${escapeHtml(String(data.description ?? ""))}

    - ${data.tokenLabel ? `

    Token: ${escapeHtml(String(data.tokenLabel))}

    ` : ""} - ${data.semanticRef ? `

    Semantic ref: ${escapeHtml(String(data.semanticRef))}

    ` : ""} - ${data.note ? `

    ${escapeHtml(String(data.note))}

    ` : ""} - ` -} - -function renderList(title: string, values: string[]): string { - if (values.length === 0) return "" - return `

    ${escapeHtml(title)}:

      ${values - .map((value) => `
    • ${escapeHtml(value)}
    • `) - .join("")}
    ` -} - -function asStringArray(value: unknown): string[] { - return Array.isArray(value) ? value.map(String) : [] -} - -function laneLabel(lane: string): string { - if (lane === "mechanical") return "Mechanical execution" - if (lane === "oracle") return "Oracle satisfaction" - if (lane === "design") return "Design exercise" - if (lane === "semantic") return "Semantic completion" - return "Revision / risk" -} - -function requireElement(id: string): HTMLElement { - const element = document.getElementById(id) - if (!element) throw new Error(`Missing element #${id}`) - return element -} - -function escapeHtml(value: string): string { - return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") -} diff --git a/archive/docs/next/prototypes/plan-petri-visualization/src/model.ts b/archive/docs/next/prototypes/plan-petri-visualization/src/model.ts deleted file mode 100644 index 37973d721..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/src/model.ts +++ /dev/null @@ -1,109 +0,0 @@ -export type Lane = "mechanical" | "oracle" | "design" | "semantic" | "revision" - -export type Place = { - id: string - label: string - lane: Lane - description: string - tokenLabel?: string - semanticRef?: string - x: number - y: number -} - -export type Transition = { - id: string - label: string - lane: Lane - description: string - compiledFrom?: string[] - guard?: string - x: number - y: number -} - -export type Arc = { - id: string - source: string - target: string - label?: string -} - -export type PetriNet = { - places: Place[] - transitions: Transition[] - arcs: Arc[] -} - -export type Scenario = { - id: string - label: string - headline: string - summary: string - valueProbe: string - marking: Set - firedTransitions: Set - notesById: Record -} - -export type TransitionState = { - id: string - enabled: boolean - fired: boolean - missingInputs: Place[] -} - -export function inputsFor(net: PetriNet, transitionId: string): Place[] { - const inputIds = net.arcs - .filter((arc) => arc.target === transitionId) - .map((arc) => arc.source) - - return inputIds - .map((id) => net.places.find((place) => place.id === id)) - .filter((place): place is Place => place !== undefined) -} - -export function outputsFor(net: PetriNet, transitionId: string): Place[] { - const outputIds = net.arcs - .filter((arc) => arc.source === transitionId) - .map((arc) => arc.target) - - return outputIds - .map((id) => net.places.find((place) => place.id === id)) - .filter((place): place is Place => place !== undefined) -} - -export function transitionStates(net: PetriNet, scenario: Scenario): TransitionState[] { - return net.transitions.map((transition) => { - const missingInputs = inputsFor(net, transition.id).filter( - (place) => !scenario.marking.has(place.id), - ) - - return { - id: transition.id, - enabled: missingInputs.length === 0, - fired: scenario.firedTransitions.has(transition.id), - missingInputs, - } - }) -} - -export function blockedTransitions(net: PetriNet, scenario: Scenario): TransitionState[] { - return transitionStates(net, scenario).filter( - (state) => !state.enabled && !state.fired && state.missingInputs.length > 0, - ) -} - -export function completionStatus(net: PetriNet, scenario: Scenario): { - done: boolean - missing: Place[] -} { - const declareDone = net.transitions.find((transition) => transition.id === "t_declare_done") - if (!declareDone) return { done: false, missing: [] } - - const missing = inputsFor(net, declareDone.id).filter((place) => !scenario.marking.has(place.id)) - return { - done: scenario.marking.has("p_plan_done"), - missing, - } -} diff --git a/archive/docs/next/prototypes/plan-petri-visualization/src/scenarios.ts b/archive/docs/next/prototypes/plan-petri-visualization/src/scenarios.ts deleted file mode 100644 index ceac66892..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/src/scenarios.ts +++ /dev/null @@ -1,489 +0,0 @@ -import type { PetriNet, Scenario } from "./model" - -const laneY = { - mechanical: 80, - oracle: 210, - design: 340, - semantic: 470, - revision: 600, -} as const - -export const sliceNet: PetriNet = { - places: [ - { - id: "p_slice_selected", - label: "Slice selected", - lane: "mechanical", - description: "The active plan slice has been chosen for orchestration.", - tokenLabel: "P1", - semanticRef: "plan_node P1", - x: 40, - y: laneY.mechanical, - }, - { - id: "p_context_ready", - label: "Context pack ready", - lane: "mechanical", - description: "A bounded context pack has been generated from the current workspace graph.", - tokenLabel: "G42", - semanticRef: "contextPackRef + graphRevision", - x: 230, - y: laneY.mechanical, - }, - { - id: "p_code_ready", - label: "Code artifact ready", - lane: "mechanical", - description: "Implementation artifacts exist. This is mechanical progress, not semantic completion.", - tokenLabel: "artifact", - x: 430, - y: laneY.mechanical, - }, - { - id: "p_verify_passed", - label: "Verify passed", - lane: "mechanical", - description: "The inner/gate verification command passed for the artifact.", - tokenLabel: "report", - x: 630, - y: laneY.mechanical, - }, - { - id: "p_oracle_required", - label: "Oracle required", - lane: "oracle", - description: "The plan graph has a gating oracle relation for this slice.", - tokenLabel: "O1", - semanticRef: "plan.verified_by_oracle P1 -> O1", - x: 230, - y: laneY.oracle, - }, - { - id: "p_oracle_evidence", - label: "Oracle evidence ready", - lane: "oracle", - description: "A report or artifact is available that can be assessed against the oracle check.", - tokenLabel: "evidence", - x: 430, - y: laneY.oracle, - }, - { - id: "p_oracle_satisfied", - label: "Oracle satisfied", - lane: "oracle", - description: "Judgment-bearing token: evidence satisfies the required oracle check for this graph revision.", - tokenLabel: "O1 ✓", - x: 630, - y: laneY.oracle, - }, - { - id: "p_design_required", - label: "Design target required", - lane: "design", - description: "The plan graph says this slice must introduce or exercise a design node.", - tokenLabel: "D2", - semanticRef: "plan.exercises_design P1 -> D2", - x: 230, - y: laneY.design, - }, - { - id: "p_design_evidence", - label: "Design evidence ready", - lane: "design", - description: "Evidence exists that can be reviewed for whether the intended seam/module was exercised.", - tokenLabel: "trace", - x: 430, - y: laneY.design, - }, - { - id: "p_design_exercised", - label: "Design exercised", - lane: "design", - description: "Judgment-bearing token: the implementation actually crossed the intended design seam.", - tokenLabel: "D2 ✓", - x: 630, - y: laneY.design, - }, - { - id: "p_intent_current", - label: "Intent current", - lane: "semantic", - description: "The targeted intent nodes are still current for the graph revision used by the work.", - tokenLabel: "R1/C1", - semanticRef: "plan.establishes_intent P1 -> R1", - x: 230, - y: laneY.semantic, - }, - { - id: "p_intent_established", - label: "Intent established", - lane: "semantic", - description: "Judgment-bearing token: required intent targets are established by the evidence bundle.", - tokenLabel: "intent ✓", - x: 630, - y: laneY.semantic, - }, - { - id: "p_completion_accepted", - label: "Completion accepted", - lane: "semantic", - description: "A reviewer or trusted assessment accepts that the plan claim is semantically complete.", - tokenLabel: "claim ✓", - x: 820, - y: laneY.semantic, - }, - { - id: "p_plan_done", - label: "Plan done accepted", - lane: "semantic", - description: "Terminal semantic marking. This can support declaring plan execution status done.", - tokenLabel: "done", - semanticRef: "plan_node.executionStatus = done candidate", - x: 1020, - y: laneY.semantic, - }, - { - id: "p_graph_current", - label: "Graph revision current", - lane: "revision", - description: "The evidence and context pack are still aligned with the current workspace graph revision.", - tokenLabel: "G42 current", - x: 40, - y: laneY.revision, - }, - { - id: "p_graph_stale", - label: "Graph stale", - lane: "revision", - description: "The context/evidence was produced from an older graph revision and needs reconciliation.", - tokenLabel: "G42 stale", - x: 230, - y: laneY.revision, - }, - { - id: "p_risk_resolved", - label: "Risk resolved / accepted", - lane: "revision", - description: "Residual risks are either retired by evidence or explicitly accepted/deferred.", - tokenLabel: "risk ✓", - semanticRef: "plan.retires_risk or accepted residual risk", - x: 820, - y: laneY.revision, - }, - ], - transitions: [ - { - id: "t_build_context", - label: "Build context", - lane: "mechanical", - description: "Compile the active plan slice and graph neighborhood into an execution context pack.", - x: 135, - y: laneY.mechanical, - }, - { - id: "t_implement", - label: "Implement", - lane: "mechanical", - description: "Dispatch coding work using the context pack.", - x: 330, - y: laneY.mechanical, - }, - { - id: "t_verify", - label: "Run verify", - lane: "mechanical", - description: "Run tests/checks that produce mechanical evidence.", - x: 530, - y: laneY.mechanical, - }, - { - id: "t_collect_oracle_evidence", - label: "Collect oracle evidence", - lane: "oracle", - description: "Route verification output into evidence for the required oracle check.", - compiledFrom: ["plan.verified_by_oracle P1 -> O1"], - x: 330, - y: laneY.oracle, - }, - { - id: "t_assess_oracle", - label: "Assess oracle", - lane: "oracle", - description: "Decide whether the evidence satisfies the required oracle check.", - guard: "Evidence maps to O1, report passes, and graph revision is current.", - compiledFrom: ["plan.verified_by_oracle P1 -> O1"], - x: 530, - y: laneY.oracle, - }, - { - id: "t_collect_design_evidence", - label: "Collect design evidence", - lane: "design", - description: "Produce evidence about whether the implementation crossed the intended seam/module.", - compiledFrom: ["plan.exercises_design P1 -> D2"], - x: 330, - y: laneY.design, - }, - { - id: "t_assess_design", - label: "Assess design", - lane: "design", - description: "Judge whether the design node was actually introduced or exercised.", - guard: "Evidence shows the intended design seam D2 was exercised, not bypassed.", - compiledFrom: ["plan.exercises_design P1 -> D2"], - x: 530, - y: laneY.design, - }, - { - id: "t_assess_intent", - label: "Assess intent", - lane: "semantic", - description: "Judge whether oracle and design evidence establish the intent target.", - guard: "Intent target is current; required oracle/design tokens are present.", - compiledFrom: ["plan.establishes_intent P1 -> R1"], - x: 530, - y: laneY.semantic, - }, - { - id: "t_review_completion", - label: "Review completion", - lane: "semantic", - description: "Accept the semantic completion claim after evidence and residual risk review.", - x: 725, - y: laneY.semantic, - }, - { - id: "t_declare_done", - label: "Declare done", - lane: "semantic", - description: "Terminal transition: declare or suggest that the plan node is done.", - guard: "All gating semantic tokens are present and graph revision remains current.", - x: 920, - y: laneY.semantic, - }, - { - id: "t_detect_stale", - label: "Detect stale graph", - lane: "revision", - description: "Detect that artifacts/evidence were produced from a stale graph revision.", - x: 135, - y: laneY.revision, - }, - ], - arcs: [ - { id: "a1", source: "p_slice_selected", target: "t_build_context" }, - { id: "a2", source: "p_graph_current", target: "t_build_context" }, - { id: "a3", source: "t_build_context", target: "p_context_ready" }, - { id: "a4", source: "p_context_ready", target: "t_implement" }, - { id: "a5", source: "t_implement", target: "p_code_ready" }, - { id: "a6", source: "p_code_ready", target: "t_verify" }, - { id: "a7", source: "t_verify", target: "p_verify_passed" }, - { id: "a8", source: "p_oracle_required", target: "t_collect_oracle_evidence" }, - { id: "a9", source: "p_verify_passed", target: "t_collect_oracle_evidence" }, - { id: "a10", source: "t_collect_oracle_evidence", target: "p_oracle_evidence" }, - { id: "a11", source: "p_oracle_evidence", target: "t_assess_oracle" }, - { id: "a12", source: "p_graph_current", target: "t_assess_oracle" }, - { id: "a13", source: "t_assess_oracle", target: "p_oracle_satisfied" }, - { id: "a14", source: "p_design_required", target: "t_collect_design_evidence" }, - { id: "a15", source: "p_code_ready", target: "t_collect_design_evidence" }, - { id: "a16", source: "t_collect_design_evidence", target: "p_design_evidence" }, - { id: "a17", source: "p_design_evidence", target: "t_assess_design" }, - { id: "a18", source: "t_assess_design", target: "p_design_exercised" }, - { id: "a19", source: "p_intent_current", target: "t_assess_intent" }, - { id: "a20", source: "p_oracle_satisfied", target: "t_assess_intent" }, - { id: "a21", source: "p_design_exercised", target: "t_assess_intent" }, - { id: "a22", source: "t_assess_intent", target: "p_intent_established" }, - { id: "a23", source: "p_intent_established", target: "t_review_completion" }, - { id: "a24", source: "p_risk_resolved", target: "t_review_completion" }, - { id: "a25", source: "t_review_completion", target: "p_completion_accepted" }, - { id: "a26", source: "p_verify_passed", target: "t_declare_done" }, - { id: "a27", source: "p_oracle_satisfied", target: "t_declare_done" }, - { id: "a28", source: "p_design_exercised", target: "t_declare_done" }, - { id: "a29", source: "p_intent_established", target: "t_declare_done" }, - { id: "a30", source: "p_completion_accepted", target: "t_declare_done" }, - { id: "a31", source: "p_graph_current", target: "t_declare_done" }, - { id: "a32", source: "t_declare_done", target: "p_plan_done" }, - { id: "a33", source: "p_context_ready", target: "t_detect_stale" }, - { id: "a34", source: "t_detect_stale", target: "p_graph_stale" }, - ], -} - -const baseFired = new Set([ - "t_build_context", - "t_implement", - "t_verify", - "t_collect_oracle_evidence", - "t_assess_oracle", - "t_collect_design_evidence", - "t_assess_design", - "t_assess_intent", - "t_review_completion", -]) - -export const scenarios: Scenario[] = [ - { - id: "happy", - label: "Happy path", - headline: "Mechanical evidence and semantic gates converge.", - summary: - "The slice reaches verification, satisfies its oracle, exercises the design seam, establishes intent, resolves risk, and can declare plan-done.", - valueProbe: - "Shows the desirable end state: Petri orchestration is not just task flow, but evidence-gated semantic completion.", - marking: new Set([ - "p_slice_selected", - "p_graph_current", - "p_context_ready", - "p_code_ready", - "p_verify_passed", - "p_oracle_required", - "p_oracle_evidence", - "p_oracle_satisfied", - "p_design_required", - "p_design_evidence", - "p_design_exercised", - "p_intent_current", - "p_intent_established", - "p_risk_resolved", - "p_completion_accepted", - "p_plan_done", - ]), - firedTransitions: new Set([...baseFired, "t_declare_done"]), - notesById: {}, - }, - { - id: "missing-oracle", - label: "Missing oracle", - headline: "Implementation can finish while semantic completion remains blocked.", - summary: - "Code and verify pass, but no oracle requirement/evidence token exists. The plan cannot become semantically done even though mechanical work looks green.", - valueProbe: - "Tests whether the UI can explain why green CI is insufficient: the required graph-derived oracle was not satisfied.", - marking: new Set([ - "p_slice_selected", - "p_graph_current", - "p_context_ready", - "p_code_ready", - "p_verify_passed", - "p_design_required", - "p_design_evidence", - "p_design_exercised", - "p_intent_current", - "p_risk_resolved", - ]), - firedTransitions: new Set(["t_build_context", "t_implement", "t_verify", "t_collect_design_evidence", "t_assess_design"]), - notesById: { - p_oracle_required: "No accepted oracle edge compiled into this run, or O1 is not implemented yet.", - t_declare_done: "Blocked despite VerifyPassed: OracleSatisfied is missing.", - }, - }, - { - id: "design-bypass", - label: "Design bypass", - headline: "Tests pass, but the intended seam was not exercised.", - summary: - "Mechanical verification and oracle evidence are present, but design assessment rejects the implementation because it bypassed the design node the slice was meant to prove.", - valueProbe: - "Surfaces architectural value: orchestration can protect design intent, not merely run tests.", - marking: new Set([ - "p_slice_selected", - "p_graph_current", - "p_context_ready", - "p_code_ready", - "p_verify_passed", - "p_oracle_required", - "p_oracle_evidence", - "p_oracle_satisfied", - "p_design_required", - "p_design_evidence", - "p_intent_current", - "p_risk_resolved", - ]), - firedTransitions: new Set([ - "t_build_context", - "t_implement", - "t_verify", - "t_collect_oracle_evidence", - "t_assess_oracle", - "t_collect_design_evidence", - ]), - notesById: { - p_design_exercised: "Assessment rejected: evidence did not show the intended seam D2 was crossed.", - t_assess_design: "Guard failed: implementation bypassed D2.", - }, - }, - { - id: "stale-graph", - label: "Stale graph", - headline: "Long-running work cannot complete against an obsolete semantic revision.", - summary: - "Context and artifacts exist, but the graph advanced during execution. Semantic transitions that require GraphRevisionCurrent are blocked until reconciliation or rebuild.", - valueProbe: - "Tests whether Petri orchestration can make stale-context hazards visible instead of silently accepting outdated work.", - marking: new Set([ - "p_slice_selected", - "p_context_ready", - "p_code_ready", - "p_verify_passed", - "p_oracle_required", - "p_oracle_evidence", - "p_design_required", - "p_design_evidence", - "p_design_exercised", - "p_intent_current", - "p_risk_resolved", - "p_graph_stale", - ]), - firedTransitions: new Set([ - "t_build_context", - "t_implement", - "t_verify", - "t_collect_oracle_evidence", - "t_collect_design_evidence", - "t_assess_design", - "t_detect_stale", - ]), - notesById: { - p_graph_current: "Missing because current graph is G43 while context/evidence was produced from G42.", - t_assess_oracle: "Blocked: oracle assessment requires current graph revision.", - t_declare_done: "Blocked: PlanDoneAccepted cannot be produced from stale evidence.", - }, - }, - { - id: "risk-pending", - label: "Risk pending", - headline: "All evidence passes, but residual risk still needs explicit treatment.", - summary: - "Intent, oracle, and design gates are satisfied. Completion review is blocked because a residual risk is neither retired nor explicitly accepted/deferred.", - valueProbe: - "Tests whether the net can represent judgment work that is not reducible to tests or code artifacts.", - marking: new Set([ - "p_slice_selected", - "p_graph_current", - "p_context_ready", - "p_code_ready", - "p_verify_passed", - "p_oracle_required", - "p_oracle_evidence", - "p_oracle_satisfied", - "p_design_required", - "p_design_evidence", - "p_design_exercised", - "p_intent_current", - "p_intent_established", - ]), - firedTransitions: new Set([ - "t_build_context", - "t_implement", - "t_verify", - "t_collect_oracle_evidence", - "t_assess_oracle", - "t_collect_design_evidence", - "t_assess_design", - "t_assess_intent", - ]), - notesById: { - p_risk_resolved: "Residual risk is still open. A human/agent must retire it or explicitly accept/defer it.", - t_review_completion: "Blocked: completion review requires risk treatment.", - }, - }, -] diff --git a/archive/docs/next/prototypes/plan-petri-visualization/src/style.css b/archive/docs/next/prototypes/plan-petri-visualization/src/style.css deleted file mode 100644 index d8091f430..000000000 --- a/archive/docs/next/prototypes/plan-petri-visualization/src/style.css +++ /dev/null @@ -1,310 +0,0 @@ -:root { - color: #172033; - background: #e5e7eb; - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; -} - -#app { - min-height: 100vh; - padding: 24px; -} - -.hero { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 24px; - align-items: end; - margin: 0 0 18px; - max-width: none; -} - -.eyebrow { - margin: 0 0 6px; - color: #4f46e5; - font-size: 12px; - font-weight: 800; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -h1, -h2, -h3, -p { - margin-top: 0; -} - -h1 { - margin-bottom: 8px; - font-size: clamp(30px, 4vw, 54px); - letter-spacing: -0.05em; - line-height: 0.95; -} - -h2 { - margin: 0 0 12px; - color: #334155; - font-size: 13px; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -h3 { - margin-bottom: 8px; - font-size: 16px; -} - -.hero p { - max-width: 780px; - color: #475569; - font-size: 17px; -} - -.scenario-picker { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 8px; - max-width: 560px; -} - -button { - border: 1px solid #cbd5e1; - border-radius: 999px; - background: #fff; - color: #334155; - cursor: pointer; - font: inherit; - font-size: 13px; - font-weight: 700; - padding: 9px 13px; -} - -button:hover, -button.active { - border-color: #4f46e5; - background: #eef2ff; - color: #312e81; -} - -.workspace { - display: grid; - grid-template-columns: 320px minmax(760px, 1fr) 340px; - gap: 14px; - height: calc(100vh - 176px); - min-height: 680px; - max-width: none; - margin: 0; -} - -.panel, -.graph-shell { - border: 1px solid rgba(15, 23, 42, 0.1); - border-radius: 24px; - background: rgba(255, 255, 255, 0.86); - box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); -} - -.panel { - overflow: auto; - padding: 20px; -} - -.panel h2:not(:first-child) { - margin-top: 26px; -} - -.graph-shell { - display: flex; - flex-direction: column; - overflow: hidden; -} - -.legend { - display: flex; - flex-wrap: wrap; - gap: 14px; - align-items: center; - border-bottom: 1px solid #e2e8f0; - padding: 12px 18px; - color: #475569; - font-size: 12px; - font-weight: 700; -} - -.dot, -.token-swatch, -.transition-swatch { - display: inline-block; - width: 10px; - height: 10px; - margin-right: 6px; - border-radius: 999px; -} - -.token-swatch, -.transition-swatch { - width: 14px; - height: 14px; - vertical-align: -2px; -} - -.transition-swatch { - border-radius: 2px; -} - -.mechanical { - background: #60a5fa; -} - -.oracle { - background: #a78bfa; -} - -.design { - background: #f59e0b; -} - -.semantic { - background: #10b981; -} - -.revision { - background: #ef4444; -} - -.token-swatch.present { - border: 2px solid #14532d; - background: #16a34a; -} - -.token-swatch.absent { - border: 2px dashed #94a3b8; - background: rgba(255, 255, 255, 0.45); -} - -.transition-swatch.fired { - border: 2px solid #1e1b4b; - background: #4338ca; -} - -.transition-swatch.idle { - border: 2px dashed #94a3b8; - background: rgba(255, 255, 255, 0.45); -} - -#cy { - flex: 1; - min-height: 0; - background: #f8fafc; -} - -.value-probe, -.note, -.blocked-card { - border-radius: 14px; - padding: 12px; -} - -.value-probe { - border: 1px solid #c7d2fe; - background: #eef2ff; - color: #3730a3; -} - -.note { - border: 1px solid #fed7aa; - background: #fff7ed; - color: #9a3412; -} - -.blocked-card { - margin-bottom: 12px; - border: 1px solid #fecaca; - background: #fef2f2; -} - -.blocked-card h3 { - color: #991b1b; -} - -.ok { - color: #047857; -} - -.empty { - color: #64748b; - font-style: italic; -} - -.pill { - display: inline-flex; - border-radius: 999px; - font-size: 12px; - font-weight: 800; - padding: 5px 9px; - text-transform: uppercase; -} - -.pill.good { - background: #dcfce7; - color: #166534; -} - -.pill.ready { - background: #dbeafe; - color: #1d4ed8; -} - -.pill.bad { - background: #fee2e2; - color: #991b1b; -} - -dl { - display: grid; - grid-template-columns: 70px 1fr; - gap: 8px; -} - -dt { - color: #64748b; - font-weight: 700; -} - -dd { - margin: 0; -} - -ul { - padding-left: 20px; -} - -li + li { - margin-top: 4px; -} - -@media (max-width: 1180px) { - .hero, - .workspace { - grid-template-columns: 1fr; - height: auto; - } - - .scenario-picker { - justify-content: flex-start; - } - - .graph-shell { - height: 720px; - } -} diff --git a/archive/docs/reference/agent-tracing-self-improvement.md b/archive/docs/reference/agent-tracing-self-improvement.md deleted file mode 100644 index 735a84b9a..000000000 --- a/archive/docs/reference/agent-tracing-self-improvement.md +++ /dev/null @@ -1,284 +0,0 @@ -# Agent tracing for self-improvement feedback - -Reference notes on wiring OpenTelemetry (OTel) tracing into a TypeScript agent stack so that (a) humans can read traces and (b) the agent itself can use its past trajectories as feedback for self-improvement. - -Compiled from `last30days` research (window: 2026-03-20 → 2026-04-19) plus design synthesis. Where a claim came from research, it is attributed; where it's a design recommendation, that's stated. - ---- - -## Landscape findings (from research) - -### OTel is the substrate everyone is converging on - -The industry signal in the last 30 days is unambiguous: **custom SDK wrappers are being deprecated in favor of standard OTel auto-instrumentation.** - -- **PostHog** merged three PRs in a two-week span explicitly moving off their own `@posthog/ai/*` wrappers toward OTel auto-instrumentation: - - `posthog-js #3349` (Apr-7) — migrate AI examples to OTel - - `posthog #53668` (Apr-8) — migrate onboarding docs to OTel auto-instrumentation - - `posthog-js #3415` (Apr-17) — migrate Gemini example to OTel -- Explicit reasoning in those PRs: *"more portable, follows industry conventions, wrappers kept as last resort."* -- **Jaeger** has an active LFX project (`jaegertracing/jaeger#8401`, Apr-17) adding GenAI-specialized trace visualization — `invoke_agent` spans, OTel-based. - -**Implication:** don't invest in provider-specific tracing wrappers. Emit OTel spans; swap backends behind OTLP. - -### Vercel AI SDK — OTel is built in - -From Pydantic's Apr-14 article *"OpenTelemetry LLM Tracing with Vercel AI SDK and Pydantic Logfire"* (https://pydantic.dev/articles/vercel-ai-sdk-logfire-otel): - -> The AI SDK's `experimental_telemetry` option works identically since it uses the global OTel tracer, regardless of how it was initialized. Three independent teams built the pieces that make this possible: Vercel added OpenTelemetry instrumentation to the AI SDK, emitting spans with `ai.*` and `gen_ai.*` attributes on every LLM call. - -Spans are emitted on every `generateText`, `streamText`, `generateObject`, `streamObject` call when `experimental_telemetry.isEnabled` is true. They carry `ai.*` (Vercel-specific) and `gen_ai.*` (OTel GenAI SIG convention) attributes. - -**Known bug (PostHog-specific, not OTel-general):** `PostHog/posthog#52442` (Mar-26, still open) — PostHog's OTel ingestion drops custom metadata (`posthog_distinct_id`, `functionId`, custom properties) from Vercel AI SDK spans. Does not affect Langfuse / Logfire / other OTLP backends. - -### OpenCode — partial OTel, known gap - -Very recent issue `marcusquinn/aidevops#19660` (Apr-18): **OpenCode v1.4.11 `run` mode does not emit per-tool-call `Tool.execute` / `Bash` spans.** Top-level spans work, but you are blind to individual tool calls in non-interactive runs. Check the current version against this thread before relying on OpenCode tracing. - -### Claude Agent SDK — no evidence in window - -The Claude Agent SDK was not mentioned in any of 62 evidence items across Reddit, GitHub, Web, TikTok. Either (a) its tracing story isn't a topic of community discussion yet, or (b) the query didn't surface it. Check Anthropic's own docs for current state. - -### Convenience layer: Traceloop (OpenLLMetry) - -Traceloop ships `@traceloop/instrumentation-*` per-provider packages. Referenced explicitly in `PostHog/posthog-js#3415`: - -> Traceloop shipped `@traceloop/instrumentation-google-generativeai`, so we can now instrument the official Google Gen AI SDK directly. - -TypeScript-native, monkey-patches the provider SDK, emits OTel spans automatically. Covers OpenAI, Anthropic, Google, Cohere, Bedrock, LangChain, LlamaIndex. Use this for SDK calls the Vercel AI SDK doesn't route through (e.g. direct Anthropic calls inside a Claude Agent SDK setup). - -### Self-improvement patterns (from research) - -Two practical references surfaced: - -1. **ChatbotKit "Self-rating Reflection Agent"** (Apr-7, https://chatbotkit.com/examples/self-rating-reflection-agent) — concrete pattern: - > A rating-aware agent should not spam the system with feedback records after every trivial exchange, and it should never fabricate evidence that is not in the rating log. Instead it should record ratings after meaningful outcomes, use clear reasons, and consult recent ratings before claiming that it has improved. - -2. **arXiv "Experiential Reflective Learning for Self-Improving LLM..."** (Mar-24, arxiv.org/pdf/2603.24639) — formal framing: - > As the agent executes tasks, it accumulates experiences consisting of the task description, the execution trajectory (reasoning steps, tool calls, and outputs), and the outcome signal. After each task, the agent reflects on this... - -Together these argue for a **rating-disciplined trajectory log**, not a firehose of every span. - -### TypeScript eval layer (DeepEval replacement) - -DeepEval is Python-only and not suitable inside a TypeScript codebase without crossing runtimes. TS-native alternatives in the same slot: - -- **Evalite** — lightweight, pytest-shaped -- **Autoevals** (Braintrust) — scoring library, framework-agnostic -- **Vercel AI SDK Evals** — first-party, integrates with the SDK - ---- - -## Architecture: two views, one capture - -The core design idea is that **humans and agents need different views of the same data**. Do not try to serve both from one UI. - -``` - ┌─────────────────────┐ - │ Vercel AI SDK │ - │ experimental_ │ - │ telemetry = true │ - └──────────┬──────────┘ - │ OTLP spans - ▼ - ┌─────────────────────┐ - │ OTel SDK │ - │ (global tracer) │ - └──────────┬──────────┘ - │ - ┌─────────────┴─────────────┐ - ▼ ▼ - ┌─────────────────────┐ ┌─────────────────────┐ - │ Langfuse / Logfire │ │ Reflection store │ - │ (human UI) │ │ (agent tool API) │ - │ — read traces │ │ — query past turns │ - │ — run evals │ │ — get ratings │ - │ — curate datasets │ │ — list failures │ - └─────────────────────┘ └─────────────────────┘ -``` - -If you pick Langfuse, its `observations` / `scores` / `datasets` APIs collapse both views into one backend — the agent queries the same store the human browses. That is the cheapest path. - ---- - -## Backend selection - -### Recommended: **Langfuse** (self-hosted) - -- Open source, Docker-compose install -- UI purpose-built for LLM traces: nested tree of prompts, completions, tool calls -- Has sessions, user IDs, evals, datasets, scores — all keyed to traces -- HTTP API is agent-friendly JSON (the same shape used by the UI) -- OTLP-native - -**Pick this** if you want one tool that covers tracing + eval + dataset curation, and if you want the agent to use the same store as its reflection memory. - -### Alternative: **Pydantic Logfire** - -- Cleaner general-purpose OTel UI -- Excellent Vercel AI SDK documentation (Apr-14 article is effectively a Logfire tutorial) -- Generous free tier, SaaS -- Less LLM-specific than Langfuse; no built-in dataset/eval management - -**Pick this** if you prioritize UI polish and are comfortable building the eval/reflection layer yourself. - -### Not recommended for this use case - -- **LangSmith** — heaviest hitter in research but tightly coupled to LangChain; skip unless already in that ecosystem. -- **Datadog / Honeycomb** — great general observability, not shaped for LLM content. -- **Jaeger (stock)** — not LLM-aware. Wait for the LFX GenAI work (`#8401`) if you want it. - ---- - -## Minimal implementation path - -### 1. Install the OTel SDK - -```ts -// src/telemetry/otel.ts -import { NodeSDK } from '@opentelemetry/sdk-node' -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' - -const sdk = new NodeSDK({ - traceExporter: new OTLPTraceExporter({ - url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, - headers: { - Authorization: `Bearer ${process.env.LANGFUSE_PUBLIC_KEY}:${process.env.LANGFUSE_SECRET_KEY}`, - }, - }), -}) - -sdk.start() -``` - -Import this once at process boot, before any Vercel AI SDK calls. - -### 2. Enable telemetry on every AI SDK call - -```ts -const result = await generateText({ - model: anthropic('claude-sonnet-4-6'), - messages, - experimental_telemetry: { - isEnabled: true, - functionId: 'review-accept', // names the span group - metadata: { - sessionId, - taskId, - userId, - }, - }, -}) -``` - -The `functionId` + `metadata.*` fields become queryable facets in Langfuse. - -### 3. Run Langfuse locally - -```bash -git clone https://github.com/langfuse/langfuse -cd langfuse && docker compose up -d -# UI at http://localhost:3000, OTLP at http://localhost:3000/api/public/otel -``` - -Point `OTEL_EXPORTER_OTLP_ENDPOINT` at the Langfuse OTLP endpoint. - -### 4. Instrument non-Vercel SDK calls with Traceloop - -For any provider call that doesn't go through the Vercel AI SDK (e.g. direct Anthropic Messages API inside a Claude Agent SDK loop): - -```ts -import * as traceloop from '@traceloop/node-server-sdk' -import Anthropic from '@anthropic-ai/sdk' - -traceloop.initialize({ disableBatch: true }) // uses the global OTel tracer -``` - -The Anthropic SDK is auto-instrumented; spans land in the same pipeline. - -### 5. Expose a reflection tool surface to the agent - -Give the agent three tools, backed by Langfuse's API: - -```ts -// Pseudocode shape — exact Langfuse query params depend on their API version. -const tools = { - get_recent_trajectories: async ({ functionId, limit }) => { - // Returns: [{ taskId, outcome, durationMs, toolCalls: [...], rating?, reason? }] - }, - get_failure_cases: async ({ since, minSeverity }) => { - // Returns low-rated trajectories with their reason field. - }, - get_rating: async ({ taskId }) => { - // Single trajectory's rating + reason, or null. - }, -} -``` - -**Critical design constraints for this surface:** - -- Return **flattened summaries**, not raw spans. One trajectory should fit in ~200 tokens. -- Include the `reason` field every time. A rating without a reason is worse than no rating. -- Cap the default `limit` low (e.g. 5). The agent should explicitly ask for more. - -### 6. Apply rating discipline - -From the ChatbotKit pattern: - -- **Rate only meaningful outcomes.** Don't rate every LLM call. Rate at task boundaries where a user-visible outcome exists. -- **Every rating has a reason.** Free-text, one sentence, written by the agent or the human. -- **Consult ratings before claiming improvement.** If the agent is going to say "I've learned X," it should be required to cite rating evidence from the tool surface. - -In Langfuse, ratings live in the `scores` API. Write to it at task boundaries only. - -### 7. Layer evals - -Pick one: - -- **Evalite** for pytest-shaped eval suites run in CI. -- **Autoevals** (Braintrust) if you want a scoring library without a full framework. -- **Vercel AI SDK Evals** if you want first-party integration. - -Write scores back to Langfuse (via its `scores` API) so rating data and trace data live in one store. - ---- - -## Gotchas - -- **PostHog-as-backend** currently drops AI SDK custom metadata (`#52442`). Use Langfuse or Logfire if you need `functionId` / session IDs to survive. -- **OpenCode `run` mode** misses per-tool-call spans (`aidevops#19660`). Check current version before committing. -- **Claude Agent SDK** tracing story is not publicly discussed in the last 30 days. Verify against Anthropic's docs when you get there. -- **DeepEval is Python-only.** Don't let its marketing pull you across runtimes. -- **Do not let the agent read the Langfuse UI JSON directly.** The UI payload is optimized for humans and will blow the context window. Always project into the flat summary shape described in §5. - ---- - -## References - -### Primary (dated within research window) - -- Pydantic, *"OpenTelemetry LLM Tracing with Vercel AI SDK and Pydantic Logfire"*, 2026-04-14 — https://pydantic.dev/articles/vercel-ai-sdk-logfire-otel -- ChatbotKit, *"Self-rating Reflection Agent"*, 2026-04-07 — https://chatbotkit.com/examples/self-rating-reflection-agent -- Confident AI, *"Top 7 LLM Observability Tools in 2026"*, 2026-04-07 — https://www.confident-ai.com/knowledge-base/top-7-llm-observability-tools -- LangChain, *"AI Agent Observability: Tracing, Testing, and Improving Agents"*, 2026-04-02 — https://www.langchain.com/articles/agent-observability -- arXiv, *"Experiential Reflective Learning for Self-Improving LLM..."*, 2026-03-24 — https://arxiv.org/pdf/2603.24639 -- `PostHog/posthog-js#3349` — migrate AI examples to OTel, 2026-04-07 -- `PostHog/posthog#53668` — migrate onboarding docs to OTel, 2026-04-08 -- `PostHog/posthog-js#3415` — migrate Gemini example to OTel, 2026-04-17 -- `PostHog/posthog#52442` — OTel ingestion drops Vercel AI SDK metadata (open), 2026-03-26 -- `jaegertracing/jaeger#8401` — GenAI trace visualization proposal, 2026-04-17 -- `marcusquinn/aidevops#19660` — OpenCode `run` mode tool-call span gap, 2026-04-18 -- `pydantic/pydantic-ai-harness#120` — agent eval/benchmarking framework, 2026-03-26 - -### Secondary (product docs) - -- Vercel AI SDK telemetry: `experimental_telemetry` option -- Langfuse: https://langfuse.com (self-hosted via `docker compose`) -- Traceloop / OpenLLMetry: `@traceloop/node-server-sdk` -- OTel GenAI semantic conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/ - -### Research caveats - -- X/Twitter source was unavailable during research (Safari cookie permissions); OTel-GenAI SIG discussion lives there and is not represented. -- YouTube returned zero results across three query variants. -- Research window is 30 days; older foundational material (e.g. original OpenLLMetry release) is outside scope. diff --git a/archive/docs/schema.dbml b/archive/docs/schema.dbml deleted file mode 100644 index 7e69edac2..000000000 --- a/archive/docs/schema.dbml +++ /dev/null @@ -1,89 +0,0 @@ -// Brunch database schema (SQLite via Drizzle) -// Source of truth: src/server/schema.ts - -Project brunch { - database_type: 'SQLite' - Note: 'Generated from src/server/schema.ts' -} - -Table specification { - id integer [pk, increment] - name text [not null] - mode text [not null, default: 'greenfield', note: 'enum: greenfield | brownfield'] - active_turn_id integer [note: 'soft ref -> turn.id (no FK in schema)'] - created_at text [not null, default: `datetime('now')`] - updated_at text [not null, default: `datetime('now')`] -} - -Table turn { - id integer [pk, increment] - specification_id integer [not null, ref: > specification.id] - parent_turn_id integer [ref: > turn.id] - phase text [not null, note: 'enum: grounding | design | requirements | criteria'] - turn_kind text [not null, default: 'question', note: 'enum: question | kickoff | recovery'] - question text [not null, default: ''] - why text - impact text [note: 'enum: high | medium | low'] - answer text - is_resolution integer [not null, default: false, note: 'boolean'] - user_parts text - assistant_parts text - created_at text [not null, default: `datetime('now')`] -} - -Table option { - id integer [pk, increment] - turn_id integer [not null, ref: > turn.id] - position integer [not null] - content text [not null] - is_recommended integer [not null, default: false, note: 'boolean'] - is_selected integer [not null, default: false, note: 'boolean'] - - indexes { - (turn_id, position) [unique, name: 'option_turn_position_unique'] - } -} - -Table phase_outcome { - id integer [pk, increment] - specification_id integer [not null, ref: > specification.id] - phase text [not null, note: 'enum: grounding | design | requirements | criteria'] - proposal_turn_id integer [not null, ref: > turn.id] - status text [not null, default: 'proposed', note: 'enum: proposed | confirmed | superseded'] - summary text [not null] - closure_basis text [note: 'enum: interviewer_recommended | user_forced'] - confirmation_turn_id integer [ref: > turn.id] - confirmed_at text - superseded_at text - created_at text [not null, default: `datetime('now')`] -} - -Table knowledge_item { - id integer [pk, increment] - specification_id integer [not null, ref: > specification.id] - kind text [not null, note: 'enum: goal | term | context | constraint | decision | assumption | requirement | criterion'] - subtype text - content text [not null] - rationale text - kind_ordinal integer [not null] -} - -Table turn_knowledge_item { - turn_id integer [not null, ref: > turn.id] - item_id integer [not null, ref: > knowledge_item.id] - relation text [not null, default: 'captured', note: 'enum: captured | confirmed | edited | invalidated | reviewed | rejected'] - - indexes { - (turn_id, item_id, relation) [pk] - } -} - -Table knowledge_edge { - from_item_id integer [not null, ref: > knowledge_item.id] - to_item_id integer [not null, ref: > knowledge_item.id] - relation text [not null, note: 'enum: depends_on | derived_from | constrains | verifies | refines'] - - indexes { - (from_item_id, to_item_id, relation) [pk] - } -} diff --git a/docs/architecture/pi-seam-extensions.md b/docs/architecture/pi-seam-extensions.md index 1ae516c38..26a9cb72f 100644 --- a/docs/architecture/pi-seam-extensions.md +++ b/docs/architecture/pi-seam-extensions.md @@ -693,7 +693,17 @@ Kernel-activation gate: behavioral kernels should not engage in earnest before a #### Edge types -Additive to the M4 edge-type catalogue: +> **Retired 2026-05-31.** The named-relation catalogue below is +> superseded by [`docs/design/GRAPH_MODEL.md`](../design/GRAPH_MODEL.md), +> which defines a closed set of eight structural edge categories +> (`dependency`, `proof`, `support`, `realization`, `boundary`, +> `composition`, `association`, `supersession`) with per-category +> policy. The named relations below map into that scheme — see +> GRAPH_MODEL.md §"Worked examples" for the oracle-plane mapping. The +> oracle-plane *nodes* (`Check`, `ValidationMethod`, `Evidence`, +> `Obligation`) defined above are not retired by this annotation. + +Additive to the M4 edge-type catalogue (now retired — see note above): - `validates` — Check → Requirement | Invariant | Criterion - `instance_of` — Check → ValidationMethod @@ -706,7 +716,7 @@ Additive to the M4 edge-type catalogue: #### Coherence rule for assumption invalidation cascade -One new rule in the coherence validator: when an `Assumption` transitions to `invalidated`, every active `Requirement` or `Invariant` with a `depends_on` edge to it must transition to `blocked` or surface a coherence violation. This is the cascade in its minimum form — a coherence rule, not an evolution engine. It composes with the M8 coherence work without inventing a separate lifecycle subsystem. +One new rule in the coherence validator: when an `Assumption` transitions to `invalidated`, every active `Requirement` or `Invariant` with a `depends_on` edge to it must transition to `blocked` or surface a coherence violation. (Under the retired catalogue this keyed on the `depends_on` named relation; under GRAPH_MODEL.md the equivalent edges are `dependency(assumption → requirement)` and `dependency(assumption → invariant)`. The cascade rule is the same.) This is the cascade in its minimum form — a coherence rule, not an evolution engine. It composes with the M8 coherence work without inventing a separate lifecycle subsystem. #### What the stub enables and what it does not diff --git a/docs/archive/PLAN_HISTORY.md b/docs/archive/PLAN_HISTORY.md index 9c2bfad1b..9cd00e2a7 100644 --- a/docs/archive/PLAN_HISTORY.md +++ b/docs/archive/PLAN_HISTORY.md @@ -3,6 +3,40 @@ This file is the active POC-line plan archive for `memory/PLAN.md`. Legacy pre-`next` history was moved out of the live docs tree with the old archived implementation. +## 2026-05-20 Origin + +- 2026-05-20 — **Pre-POC archive and reseed** — razed pre-POC implementation, archived legacy docs and planning memory under `archive/`, tagged `next-baseline`, and reseeded `memory/SPEC.md` and `memory/PLAN.md` from the three canonical POC architecture docs. Phase 3 infra bootstrap was folded into `walking-skeleton` rather than remaining an independent frontier. + +## 2026-05-22 Sync archive + +Archived out of `memory/PLAN.md` when `web-shell` closed and the live frontier advanced to `graph-data-plane`. + +- 2026-05-22 — **web-shell judo review fixes** — Session projection reads share a canonical Brunch session envelope, prompt-side custom-entry classification uses an explicit allowlist, and the React shell builds transcript query params from a typed session projection target without non-null assertions. Verified: `npm run verify` after each slice. +- 2026-05-22 — **web-shell tie-off queue** — Explicit session projection rejects ambiguous self-description (`brunch.session_binding` duplicates, missing/duplicate Pi headers, binding/header session-id mismatch); `session.transcriptDisplay` includes displayable transcript-native `brunch.elicitation_prompt` rows; M3 browser-open smoke debt was adjudicated as environment-blocked after direct HTTP/WebSocket postconditions passed. +- 2026-05-22 — **web-shell hardening slices** — Shared JSON-RPC protocol helpers, `ws`-backed `/rpc` transport, persistent browser RPC multiplexing, traversal-safe static asset serving, stable React runtime ownership, and explicit read-only session projection by durable session id landed without REST product reads or connection-as-session semantics. +- 2026-05-21 — **web-shell initial slices** — Linear transcript policy hardening landed before browser consumption, transcript readers fail fast on non-linear Pi JSONL, the minimal native web HTTP shell and WebSocket RPC bridge came online, and the React shell rendered `workspace.snapshot` chrome via one WebSocket RPC client. +- 2026-05-20 — **walking-skeleton** — Brunch launches through a pi-backed TUI boot path with coordinator-first spec gating, project-local `.brunch/` state, self-describing Pi JSONL sessions, same-spec `/new`, persistent chrome through pi's extension widget seam, a bin shim, and the store-only runbook checker. Verified: `npm run verify`, manual TUI smoke, automated TUI/coordinator tests, and runbook oracle. + +## 2026-06-01 Sync archive + +Archived from `memory/PLAN.md` when `pi-ui-extension-patterns` tied off (FE-744) and the live plan promoted `sealed-pi-profile-runtime-state` to Active with an expanded scope that absorbs graph-model prep before `graph-data-plane` (M4) CRUD begins. + +### pi-ui-extension-patterns + +- **Name:** Prove Pi extension patterns for Brunch UI affordances +- **Linear:** [FE-744](https://linear.app/hash/issue/FE-744/pi-ui-extension-patterns) +- **Branch:** `ln/fe-744-pi-ui-extension-patterns` (off `ln/fe-737-web-shell`, parallel to `ln/fe-741-graph-data-plane`) +- **Kind:** structural (spike-flavored) +- **Status:** done — implementation-complete and tied off. All Pi extension seam evidence for M5/M6/M7 has landed: command-containment, dynamic chrome semantics, hierarchical spec/session picker startup + in-session flow, RPC/headless initial-selection contract, pty startup oracle, centered branded overlay reuse, evidence-memo reconciliation, structured-exchange schema/builder, TUI/editor adapters, live Pi RPC editor fallback, response-side projection, option-selection comments, structured-exchange editor fallback, raw Pi RPC structured-exchange evaluator proof, discoverable structured-exchange extension source at `src/tui-client/.pi/extensions/structured-exchange/index.ts`, public Brunch RPC structured-exchange tuple parity through the current deterministic permutation set, parity hardening for distinct exchange ids, terminal non-answered statuses, option content/rationale, no repeated deterministic prompts, committed `.fixtures` public-RPC parity probe artifacts, web real-time observation of RPC-originated structured-exchange transcript updates, private code-composed Brunch prompt-pack topology under `src/tui-client/.pi/context/`, the Zod-authored structured-exchange schema layer under `src/tui-client/.pi/extensions/structured-exchange/schemas/`, shared Brunch identity primitives, branded persistent chrome, and Brunch-host branded startup pty evidence. Strict built-in command suppression remains an A18-L Pi API residue carried forward to the embedded-harness prep envelope. +- **Objective:** Demonstrate the Pi extension seams and Brunch product RPC seams needed before M5/M6/M7 depend on them: product-named commands routed through Brunch handlers; effect blocking for unsupported branch/session flows; dynamic Brunch-owned chrome through one wrapper; Brunch-owned startup/session selection; structured elicitation where system/assistant-originated questions use Pi transcript truth and TUI/RPC adapters; and a public Brunch JSON-RPC structured-exchange loop where an agent-as-user discovers methods, activates workspace/spec/session, starts/resumes assistant-first elicitation, answers pending structured exchanges through Brunch methods, and leaves transcript/projection evidence for current exchange permutations comparable to a TUI session. +- **Acceptance:** `docs/architecture/pi-ui-extension-patterns.md` catalogs the evidence with verdicts (`proven` / `feasible-with-cost` / `requires-pi-change` / `not-feasible`), distinguishes strict command suppression from lifecycle effect blocking, records the minimum upstream Pi command/keybinding policy ask, and captures the RPC degradation profile for chrome/custom UI. Brunch code exposes a product-named extension entrypoint plus wrappers for chrome, command policy, session lifecycle binding, and `/brunch`; the centered spec/session picker supports an optional continue-last fast path plus hierarchical create-spec/resume-spec/create-session/resume-session decisions without UI-owned session mutation and is shared by startup plus in-session adapters; TUI startup runs a Brunch-owned pre-Pi gate before `InteractiveMode` so prior transcript rendering is opt-in rather than implicit; creating a new session lands in a binding-only session for the selected spec; chrome receives the activated session id instead of fabricating `unbound`; the startup no-resume pty oracle proves stale transcript text is absent before explicit activation. Public RPC structured-exchange parity is covered: `rpc.discover` describes the supported Brunch JSON-RPC surface with method descriptions, param/result schemas, and examples; `workspace.selectionState` / `workspace.activate` let the driver enter a new workspace→spec→session without invoking TUI picker code; `session.startElicitation`, `session.pendingExchange`, and `elicitation.respond` expose an assistant-first pending-exchange lifecycle over Brunch methods, not raw Pi commands; the deterministic agent-as-user driver answers the current structured-exchange permutations through Brunch JSON-RPC only and reports blockers/frictions; the resulting Pi JSONL plus `session.transcriptDisplay` and `session.elicitationExchanges` projections preserve prompt/question/option content/rationale/answer/comment/mode/status artifacts at TUI-comparable quality. Web clients receive real-time product update notifications for RPC-originated structured-exchange mutations and refetch canonical projection handlers rather than reading from a parallel view store. Branded/themed chrome has been recovered through shared identity primitives, persistent chrome wrapper updates, and a Brunch-host branded startup pty oracle; persistent activated chrome has only qualitative manual-polish debt remaining. +- **Verification:** Inner — verify gate plus unit tests for extension wrappers; coordinator inventory/activation tests for switch decisions; source/contract tests that switcher UI returns decisions rather than mutating sessions; schema tests for structured question result details and JSON-editor request/response parsing. Middle — probe oracles per affordance category (manual checklist + executable postcondition checker on chrome state, JSONL tool results/custom entries emitted, or command-result discriminants); contract tests for Brunch handler shapes (`rpc.discover`, picker selection, elicitation start/pending/respond relay, transcript projections); pty/ANSI-stripped startup oracle proving no prior transcript appears before an explicit resume/open decision; raw Pi RPC probe demonstrating `ctx.ui.editor` JSON fallback round-trips through the documented extension UI protocol as supporting evidence only; scripted TUI demo covering all supported structured-exchange permutations; deterministic public Brunch RPC agent-as-user parity probe where the evaluator has a mission/intention, critical UX or feature-evaluation focus, permutation-bounded turn budget, and blocker/friction report; parity oracle over the saved Pi JSONL plus transcript/exchange projections, including no repeated deterministic prompts; web real-time update smoke proving browser state changes when selected session/exchange state changes via RPC-originated structured-exchange mutations; TUI-originated observation remains covered only if it reuses the same product invalidation path. Outer — manual TUI walkthrough validating visual quality, full-screen startup feel, interaction feel, and controllability cost between scripted-driver and manual paths. +- **Cross-cutting obligations:** Preserved the linear-transcript invariant (`I19-L`) — no branch creation, no mid-turn state mutations outside the command layer, no parallel chat/turn store. Preserved the workspace hierarchy and startup invariant (`R19` / `I22-L`): the workspace is the cwd, not a user-created selectable object; `.brunch/state.json` is default acceleration, not implicit resume; no prior transcript or agent loop may run before an explicit spec/session activation decision. Spec/session picker UI remained pure decision rendering; `WorkspaceSessionCoordinator` owns inventory, activation, state writes, session creation/opening, and binding. RPC/headless startup exposes structured initial-selection state/results, not the TUI picker. Structured-exchange affordances use Pi transcript truth first: `toolResult.details` is the canonical structured response payload, including optional user `comment` fields for option-selection exchanges. Slash commands and action buttons route writes through the `CommandExecutor`; the JSON-editor RPC fallback is an adapter over Pi's supported extension UI protocol, not a new public Pi command family. Public agent-as-user probes speak Brunch JSON-RPC (`rpc.discover`, `workspace.*`, `session.*`, `elicitation.*`) and delegate to Pi RPC only behind Brunch adapters. Custom-entry kinds declare `lens` per `I18-L` if elicitor-emitted. Establishment-offer affordances stay orientation-first and user-invoked when expanded. TUI chrome/status affordances call Brunch product wrappers rather than raw Pi `ctx.ui.*` primitives; the chrome wrapper does not publish its own `brunch.chrome` status key. +- **Why now / unlocks:** Lens/review-set/reviewer UX in M5 and authority gating in M6 both assume Brunch can render rich interactive affordances over Pi without forking it. Proving the affordance set early de-risked those frontiers and let the agent-as-user-driver extension question (controllability vs cost trade flagged in `ln-oracles` pass) be answered with evidence rather than estimation. +- **Traceability:** R4, R14, R16, R17, R19, R20, R21, R24, R27, R28 / D2-L, D5-L, D11-L, D12-L, D13-L, D17-L, D19-L, D21-L, D22-L, D24-L, D25-L, D26-L, D27-L, D29-L, D32-L, D33-L, D34-L, D35-L, D36-L, D37-L, D38-L, D39-L, D40-L, D41-L, D48-L, D49-L, D50-L / I10-L, I13-L, I18-L, I19-L, I22-L, I23-L, I24-L, I25-L, I26-L, I32-L, I33-L / A14-L, A17-L, A18-L, A19-L +- **Design docs:** [pi-seam-extensions.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md), [pi-ui-extension-patterns.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-ui-extension-patterns.md), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) +- **Final execution pointer:** FE-744 landed the public-RPC structured-exchange parity spine and its hardening end to end (see Status above for the full landed-work catalogue). Strict built-in command suppression (A18-L) was accepted as a residual Pi API risk and carried forward to the `sealed-pi-profile-runtime-state` prep envelope. Future expansions of capture-analysis payloads, shared transcript component subparts, or the runtime migration from tuple details to the new Zod exports require a separate `ln-design` pass before implementation. + ## 2026-05-28 Sync archive Archived from `memory/PLAN.md` so the live plan only carries active, next, horizon, and recent-completion state. @@ -52,7 +86,7 @@ Archived from `memory/PLAN.md` so the live plan only carries active, next, horiz - **Verification:** Inner — verify gate plus synthetic JSONL projection tests. Middle — JSONL round-trip/property tests for raw payloads, `brunch.session_binding`, structured elicitation entries, defensive branch-shape projection behavior, coordinator-created `/new` sessions, and M1 fixture replay parity. Outer — fixture replay parity across the transcript-first run bundle; no new human review was required because brief content and scripted user notes did not change. - **Cross-cutting obligations:** This frontier is the transcript-side proof for the shared event substrate that later carries structured elicitation entries, session binding, lens switches, side-task results, mentions, and `worldUpdate` without inventing a parallel channel or canonical chat/turn store. JSONL viability must validate sessions created through the `WorkspaceSessionCoordinator`, including the first-entry binding and `/new` same-spec behavior. - **Traceability:** R7, R8, R16, R17, R19 / D6-L, D11-L, D12-L, D13-L, D18-L, D24-L / I3-L, I8-L, I10-L, I19-L -- **Design docs:** archived [jsonl-session-viability-note](file:///Users/lunelson/Code/hashintel/brunch-next/archive/archive/docs/architecture/jsonl-session-viability-note.md) +- **Design docs:** legacy `next/architecture/jsonl-session-viability-note.md` on the `main` branch of `hashintel/brunch` - **Current execution pointer:** complete; proceed to `web-shell`. ### web-shell diff --git a/docs/design/GRAPH_MODEL.md b/docs/design/GRAPH_MODEL.md new file mode 100644 index 000000000..5a2646407 --- /dev/null +++ b/docs/design/GRAPH_MODEL.md @@ -0,0 +1,788 @@ +# Graph Model + +Canonical reference for the Brunch graph data plane (M4 and onward). +Owns both the edge layer and the node layer end-to-end. + +This document is the lock for the graph model that supersedes the +prior "large semantic edge-type catalogue + relation-policy registry" +direction and the deferred `framing_as` modality. It is also the +source of truth for the type definitions under +[`src/graph/`](../../src/graph/) and the per-category policy table +consumed by snapshot/projection builders and the `CommandExecutor`. + +`memory/SPEC.md` and `memory/PLAN.md` are reconciled to this doc; +if later planning text drifts, treat this document as the canonical +graph-model contract. + +## Status + +- **Phase 1:** edges, edge policy, reconciliation-need shape. Locked. +- **Phase 2:** per-plane node kinds, node shape, detail schemas, + kind categories, `source` field, `provenance` retirement. Locked. + +## Scope and posture + +The primary author of graph nodes and edges in Brunch is an LLM +agent — through direct elicitation, post-exchange capture, review-set +proposals, and reviewer findings. Two pressures follow from that: + +1. **Authoring burden must be low.** The agent should not be asked + to choose among many named relation kinds, each with its own + tuple-specific legality and its own projection policy. +2. **Interpretation burden at snapshot time must be low.** Context + builders should derive dependency/dependent/support/realization + buckets from the stored edge's category and endpoint roles, not + from a per-relation policy registry. + +This drives the design move: store a small closed set of +**structural edge categories with endpoint roles**. Derive +domain-specific labels later from tuple context (see +[§Tuple-label lookup](#tuple-label-lookup)). The category drives all +policy. + +## Atoms + +```ts +type NodeId = string +type EdgeId = string +type Lsn = number // monotonic, one per commit +``` + +## GraphEdge — the single shape + +```ts +type EdgeCategory = + | "dependency" + | "proof" + | "support" + | "realization" + | "boundary" + | "composition" + | "association" + | "supersession" + +type EdgeStance = "for" | "against" // required for proof | support +type EdgeBasis = "explicit" | "accepted_review_set" + +interface GraphEdge { + readonly id: EdgeId + readonly category: EdgeCategory + readonly sourceId: NodeId + readonly targetId: NodeId + readonly stance?: EdgeStance // REQUIRED for proof, support + // INVALID for other categories + readonly basis: EdgeBasis + readonly rationale?: string + readonly createdAtLsn: Lsn + readonly updatedAtLsn: Lsn // metadata only; + // category/source/target/stance are immutable + // (change category = delete + recreate) +} +// provenance (sessionId, entryId, proposalEntryId) is retired from +// the edge shape. The change_log at createdAtLsn owns all audit +// trail; transcript entry pointers are fragile under compaction. +``` + +### What this shape does NOT carry + +The prior model carried fields that are dropped here. Each has a +named successor home; nothing is lost, the substrates are different. + +| Dropped field | Successor home | +| ------------------------- | -------------------------------------------------------------------- | +| `status: proposed` | Review-set drafts (D27-L) — not a graph edge yet | +| `status: accepted` | The edge simply exists | +| `status: rejected` | Edge was never created, or was deleted. Audit lives in `change_log` | +| `status: stale` | A `ReconciliationNeed` with `target.kind = "edge"` references it | +| `support: weak_candidate` | Lives in structured-exchange `preface` or `capture_*` analysis (D47-L, D50-L) | +| `support: strong_inference` | Same — promoted to accepted edge only via review set | +| `relation: ` | Collapsed into category + stance + endpoint roles | +| `family` | Implied by category | +| Per-relation policy axes | Per-category policy table below | + +Authority for edge writes lives in the `change_log` keyed by +`createdAtLsn` / `updatedAtLsn`. Edges do not denormalize authority. + +## Edge categories — directional first + +Seven directional categories and one symmetric. The first cut for +the agent is the cardinality question — *directional or peer?* — +which routes to the right rubric before any subtler discrimination. + +| Category | Source role | Target role | Meaning | +| -------------- | ------------ | ------------ | --------------------------------------------------------- | +| `dependency` | dependency | dependent | Hard upstream — downstream validity depends on it | +| `proof` | oracle | claim | Oracle-bearing witness (`for`) or refutation (`against`) | +| `support` | support | claim | Motivation, rationale, evidence; not load-bearing | +| `realization` | abstract | concrete | Expression / implementation / establishment / assertion | +| `boundary` | boundary | subject | Scope rule, constraint, exclusion, limit | +| `composition` | whole | part | Containment / decomposition (NOT sequencing) | +| `supersession` | successor | predecessor | Intentional replacement; acyclic | +| `association` | peer | ↔ peer | Weak relatedness; last resort | + +Notes on the categories most likely to be confused: + +- **`proof` vs `support`.** Both have stance. `proof` is + oracle-bearing — the source is an artifact whose function is to + witness (or refute) the claim: a `criterion`, a `check`, an + `example` used as positive witness or counterexample, a piece of + `evidence`. `support` is rationale-bearing — the source explains + why the target exists or motivates it without being load-bearing: + a `context`, a goal-shaped motivation, a non-oracle example used + for illustration. If invalidating the source should drive a + `criteria-help` signal or progressive-checkability rendering, it + is `proof`; if it just changes the rationale, it is `support`. +- **`dependency` vs `support`.** Both run upstream-to-downstream. + Use `dependency` only when downstream validity or readiness + depends on the upstream. Use `support` when the upstream explains + or motivates but the downstream could still stand if the support + changed. +- **`realization` vs `composition`.** Composition is whole/part. + Realization is abstract/concrete (same conceptual thing, + different level of specification). A milestone *composes* its + slices; a requirement is *realized* by a slice. +- **`boundary` vs `support:against`.** Boundary is when the source + is itself a scope rule / constraint / non-goal. `support:against` + is when the source is evidence or an example that argues against + the claim. +- **`supersession` vs `realization`.** Supersession is temporal / + evolutionary replacement of overlapping scope. Realization is + abstract-to-concrete elaboration. A new requirement *supersedes* + an old requirement it replaces; a module *realizes* the + requirement (no replacement). + +## Per-category policy + +The category drives all policy. The `CommandExecutor` enforces +structural legality at write time; snapshot/projection builders use +this table to bucket edges; coherence triggers use the cascade +column. + +| | cascade on src change | recon_need on src change | criteria-help signal | projection effect | +| -------------- | :-------------------: | :----------------------: | :------------------: | ---------------------------------------------- | +| `dependency` | ✓ | ✓ | — | — | +| `proof` | — | advisory | ✓ | — | +| `support` | — | advisory | — | — | +| `realization` | — | advisory | — | — | +| `boundary` | — | ✓ | — | — | +| `composition` | — | — | — | — | +| `association` | — | — | — | — | +| `supersession` | — | — | — | hide predecessor from active context | + +Legend: + +- **cascade** — automatic block / mark stale on the dependent + (e.g. assumption invalidation cascade). +- **recon_need on src change** — generate a `ReconciliationNeed` + pointing at the edge. *Advisory* = generated only if a coherence + rule asks for it; the edge does not auto-cascade. +- **criteria-help** — used by the interviewer to suggest criteria + for the target node ("requirement with no `proof` incoming → + suggest criterion"). +- **projection effect** — how snapshot/neighborhood builders treat + the edge in active-context views. + +Only `dependency` triggers automatic cascades. Other categories +surface as reconciliation needs at most; they do not auto-block +downstream items. + +## Worked examples — same shape across planes + +```text +# Intent (M4) +A_local_only : assumption -[dependency]-> D_no_auth : decision +C_no_cloud : constraint -[boundary]-> D_no_auth : decision +I_no_network : invariant -[realization]-> R_offline : requirement +CR_airplane : criterion -[proof:+]-> I_no_network : invariant +E_typical : example -[proof:+]-> R_offline : requirement +E_outage : example -[proof:-]-> A_local_only : assumption + +# Oracle (M5+ stub) — folds prior validates/instance_of/produces/discharges +CR_airplane : criterion -[realization]-> CH_airplane : check +CH_airplane : check -[proof:+]-> I_no_network : invariant +CH_airplane : check -[realization]-> VM_unit : validation_method +CH_airplane : check -[realization]-> EV_trace : evidence +CH_airplane : check -[proof:+]-> OB_no_network : obligation +OB_no_network : obligation -[realization]-> I_no_network : invariant + +# Design (M5+ stub) +R_offline : requirement -[realization]-> M_sqlite_store : module +IF_session_store : interface -[realization]-> M_sqlite_store : module +M_sqlite_store : module -[composition]-> M_sqlite_helper: module +M_sqlite_store : module -[dependency]-> M_pi_session : module + +# Plan (M5+ stub) +MS_graph : milestone -[composition]-> FE_700 : frontier +FE_700 : frontier -[composition]-> SL_persist : slice +R_offline : requirement -[realization]-> SL_persist : slice +SL_persist : slice -[supersession]-> SL_persist_v0 : slice +``` + +No plane-crossing rules. Node `kind` does not constrain edge +category legality. The categories were chosen so that the +cross-plane vocabulary stays naturally narrow. + +## ReconciliationNeed — separate substrate, NOT a graph edge + +```ts +type ReconciliationNeedKind = + | "edge_revalidation" // existing edge needs re-checking + | "possible_relation" // two nodes might need an edge + | "possible_duplicate" // two nodes might be the same + | "semantic_conflict" + // open extension + +type ReconciliationNeedTarget = + | { readonly kind: "edge"; readonly edgeId: EdgeId } + | { readonly kind: "node_pair"; readonly aId: NodeId; readonly bId: NodeId } + +interface ReconciliationNeed { + readonly id: string + readonly kind: ReconciliationNeedKind + readonly target: ReconciliationNeedTarget + readonly rationale?: string + readonly createdAtLsn: Lsn + readonly resolvedAtLsn?: Lsn +} +``` + +Reconciliation needs reference graph state. They are **not** graph +edges. They do not appear in projection neighborhoods as edges. +They surface to the user through next-turn delivery as advisory +items, per D29-L. + +`target.kind = "edge"` is the default — recon_needs describe +relations whose semantic basis may have changed. `target.kind = +"node_pair"` covers the cases where no edge exists yet (possible +duplicate, possible relation). When a `node_pair` need resolves to +"yes, edge exists," create the edge and close the need; an audit +choice about whether to rewrite the need's target to `edge` form is +deferred. + +## Tuple-label lookup + +Tuple-label lookup is a presentation concern only. It produces +plain-language phrasing for graph snapshots, UI, and prompt +context. It does not change category policy; it only renders the +stored edge readably from one endpoint's perspective. + +Examples: + +| Stored edge | View from source | View from target | +| ------------------------------------------ | ----------------------- | -------------------------- | +| `dependency(assumption → decision)` | "premise for decision" | "depends on assumption" | +| `dependency(assumption → requirement)` | "required by requirement"| "depends on assumption" | +| `support(context → requirement, for)` | "motivates requirement" | "motivated by context" | +| `proof(criterion → invariant, for)` | "witnesses invariant" | "witnessed by criterion" | +| `proof(example → invariant, against)` | "counterexample for invariant" | "challenged by counterexample" | +| `realization(invariant → requirement)` | "expressed by requirement" | "expresses invariant" | +| `realization(requirement → design module)` | "realized by module" | "realizes requirement" | +| `realization(interface → adapter)` | "implemented by adapter"| "implements interface" | +| `realization(requirement → plan slice)` | "established by slice" | "establishes requirement" | +| `boundary(non-goal → requirement)` | "rules out / limits" | "bounded by non-goal" | +| `composition(milestone → slice)` | "contains slice" | "belongs to milestone" | +| `supersession(new req → old req)` | "supersedes prior" | "superseded by" | +| `association(A ↔ B)` | "related to B" | "related to A" | + +The lookup is a static table keyed on +`(category, source.kind, target.kind, perspective[, stance])`. It is +built by inverting the prior catalogue entries plus the `proof` rows +above. It lives separately from this document — the canonical +location for the table is TBD (likely +`src/graph/projection/labels.ts` when projection builders land). + +### Realization sub-types — tuple-implied, not edge-encoded + +The prior brainstorm proposed splitting `realization` into +`implementation`, `establishment`, `assertion`, and `expression`. +The current bet is that those distinctions emerge from tuple +context — e.g. `realization(requirement → module)` reads as +"implementation," `realization(requirement → slice)` reads as +"establishment" — and therefore live in the label-lookup table +rather than as an edge-shape field. If probe runs surface three or +more realization sub-clusters that demand distinct cascade or +projection policy, split `realization` into siblings (see +[§Open questions](#open-questions)). + +## Snapshot bucketing + +Snapshot buckets come from category and endpoint role, not from the +derived label string. A neighborhood snapshot of an intent node: + +```text +anchor: R_offline : requirement + +hard dependencies: + A_no_network depends on assumption + +support: + P_field_users motivated by context + +proof: + CR_airplane witnessed by criterion + E_typical witnessed by example + +realized by: + M_sqlite_store realized by design module + SL_persist established by plan slice + +boundaries: + C_no_cloud bounded by constraint + +supersedes: + R_offline_v0 supersedes prior requirement +``` + +## Structural invariants + +- Edge categories are closed. Agents cannot submit arbitrary + relation strings. +- Every edge has exactly one category. +- `stance` is required iff `category ∈ { proof, support }`. +- `association` is symmetric at the product level even if stored + with `sourceId` / `targetId` columns. +- `supersession` chains are acyclic. +- Accepted graph edges are graph truth. Candidate or low-confidence + edges live outside graph truth (preface / capture analysis / + review-set drafts) until accepted. +- Tuple-label lookup cannot change category policy. +- Snapshot bucket assignment comes from category and endpoint role, + not from label strings. +- `composition` does not imply sequencing or dependency. +- `support` does not imply blocking / staleness by default. +- Only `dependency` triggers automatic cascades; other categories + surface as `ReconciliationNeed` records when policy says so. +- Cross-plane freedom: node `kind` does not constrain edge + category legality. + +## Agent-facing command surface + +Prefer category-specific commands over one generic +`createEdge({ category })` command — the call site documents the +intended role: + +```ts +linkDependency({ dependency, dependent, basis, rationale }) +linkProof({ oracle, claim, stance, basis, rationale }) +linkSupport({ support, claim, stance, basis, rationale }) +linkRealization({ abstract, concrete, basis, rationale }) +linkBoundary({ boundary, subject, basis, rationale }) +linkComposition({ whole, part, basis, rationale }) +linkAssociation({ a, b, basis, rationale }) +linkSupersession({ successor, predecessor, basis, rationale }) +``` + +The command layer owns structural validation. If a tuple is +structurally illegal (missing stance, supersession cycle, etc.) the +tool returns `structural_illegal`; the agent should not invent a +narrower category to force the write through. + +These commands land in the M5 `agent-graph-integration` extension +under `src/.pi/extensions/graph/tools/` per D52-L. They are out of +scope for Phase 1 stubs. + +### `commitGraph` — atomic batch mutation (D53-L) + +The `propose-graph` strategy's load-bearing tool. One tool call +creates an entire subgraph — nodes and edges — in a single +transaction with one LSN. + +```ts +commitGraph({ + nodes: [ + { ref: "n1", kind: "requirement", title: "...", body: "..." }, + { ref: "n2", kind: "constraint", title: "...", body: "..." }, + { ref: "n5", kind: "invariant", title: "...", body: "..." }, + { ref: "n3", kind: "decision", title: "...", body: "...", + detail: { chosen_option: "...", rejected: ["..."], rationale: "..." } }, + { ref: "n4", kind: "term", title: "...", + detail: { definition: "...", aliases: ["..."] } }, + ], + edges: [ + { category: "dependency", source: "n1", target: "n2" }, + { category: "boundary", source: "n2", target: "n1" }, + { category: "realization", source: "n1", target: "n3" }, + { category: "support", source: { existing: "A12" }, target: "n1", + stance: "for" }, + ] +}) +``` + +Reference modes: +- **Intra-batch**: `"n1"` — a node defined in the same payload +- **Existing**: `{ existing: "A12" }` — a node already in the graph + +CommandExecutor processing: + +``` +commitGraph tool call + │ + ▼ + 1. Validate all nodes structurally + 2. Assign real NodeIds to each batch ref + 3. Resolve intra-batch refs on edges + 4. Resolve existing-node refs (fail if not found) + 5. Validate all edges (closed categories, stance, acyclicity) + 6. Allocate ONE Lsn + 7. Write all nodes + edges + change-log in one transaction + 8. Return success + created ids + OR structural_illegal + diagnostics for retry +``` + +All-or-nothing (I34-L): if any node or edge fails, the entire batch +is rejected. The agent may retry within a bounded budget; the user +does not see intermediate failures. + +`commitGraph` and `acceptReviewSet` (D27-L) are parallel paths to the +same CommandExecutor — one for direct agent-authored commits after +concept acceptance, one for user-reviewed batch proposals. + +## Prompting + +System-prompt fragment for graph-writing agents: + +```text +When creating graph edges, choose only from Brunch's structural edge categories: +dependency, proof, support, realization, boundary, composition, association, supersession. + +Do not invent relation names such as depends_on, validates, witnesses, implements, +expresses, motivated_by, or related_concern. Those are rendering labels derived later +from the stored category and endpoint node kinds. + +Create an accepted graph edge only when the relation is clear enough to become graph truth. +If the relation is weak, speculative, ambiguous, or merely a possible duplicate / possible +relation, do not create an accepted edge. Keep it in preface / capture analysis or raise a +reconciliation_need. + +Use one edge for the strongest operational role between two nodes. Do not create multiple +edges merely because several English paraphrases are possible. +``` + +Category-selection rubric (ask in order; stop at first strong match): + +```text +0. Should this be graph truth now? + - explicit user statement, accepted review set, or high-confidence extraction -> continue + - weak inference, possible relation, possible duplicate, unresolved ambiguity -> no accepted edge + +1. Is a newer item intentionally replacing an older item for overlapping scope? + -> supersession(successor -> predecessor) + +2. Is this a whole/part, parent/child, or decomposition relation? + -> composition(whole -> part) + +3. Does one item limit, exclude, scope, or constrain another? + -> boundary(boundary -> subject) + +4. If the upstream item is invalidated, must the downstream be revisited/blocked/stale? + -> dependency(dependency -> dependent) + +5. Is the source an oracle artifact that witnesses or refutes the claim + (criterion, check, example as witness/counterexample, evidence)? + -> proof(oracle -> claim, stance: for | against) + +6. Is one item a concrete expression, implementation, assertion, or establishment of another? + -> realization(abstract -> concrete) + +7. Does one item motivate, justify, evidence, or challenge another without being load-bearing + and without being an oracle? + -> support(support -> claim, stance: for | against) + +8. Are the two items usefully related, but no stronger role is safe? + -> association(a <-> b) + +9. Otherwise, create no edge. +``` + +## Naming notes + +- **`proof` collides with the prior `proof` checkability tier.** If + the progressive-checkability ladder lands as node metadata in + Phase 2 or later, rename its tier to `formal_proof` to avoid + collision with the edge category. The category name *proof* + covers any oracle-bearing witness; *formal_proof* is one rung at + the strong end of the checkability ladder. + +## GraphNode — the single shape + +```ts +interface GraphNode { + readonly id: NodeId + readonly plane: NodePlane + readonly kind: string // per-plane closed enum (see below) + readonly title: string // required, non-empty + readonly body?: string // markdown content + readonly basis: NodeBasis + readonly source?: string // free-form epistemic attribution + // convention by prompt, not structural validation + // e.g. "stakeholder", "regulatory", "derived" + readonly detail?: object // per-kind validated sub-structure (JSON column) + readonly createdAtLsn: Lsn + readonly updatedAtLsn: Lsn +} + +type NodePlane = "intent" | "oracle" | "design" | "plan" +type NodeBasis = "explicit" | "accepted_review_set" +// Same semantics as EdgeBasis — how the node entered graph truth. +// No "inferred" basis; low-confidence material stays in preface / +// capture analysis until promoted. +``` + +### Fields + +- **`plane`** — which graph plane owns this node. Structurally + validated; determines which `kind` enum applies. +- **`kind`** — per-plane closed enum. Structurally validated by + the `CommandExecutor`. See [§Per-plane node kinds](#per-plane-node-kinds). +- **`title`** — required, non-empty. The human-readable name of the + node. Used for mentions, snapshot display, and search. +- **`body`** — optional markdown content. Carries the semantic detail + the agent authored. Most kinds put their primary content here. +- **`basis`** — how the node entered graph truth. Same `explicit` / + `accepted_review_set` semantics as edges. +- **`source`** — free-form string for epistemic attribution. + Convention by prompt (e.g. "stakeholder", "regulatory", "derived", + "domain expert", "market research", "agent synthesis"), not + structural validation. Exists for context-snapshot enrichment — + it will be transformed back into sparse text in prompt snapshots, + not used for policy or filtering. +- **`detail`** — optional JSON object with per-kind validated + sub-structure. See [§Per-kind detail schemas](#per-kind-detail-schemas). +- **`provenance`** — retired. The `change_log` at `createdAtLsn` + owns all audit trail. Transcript entry pointers (sessionId, + entryId, proposalEntryId) are fragile under compaction and + redundant with `change_log` + `basis`. + +## Per-plane node kinds + +### Intent plane + +Intent kinds fall into three **derived categories** that map to +spec-grade progression. Category is a pure function of `kind` — it +is not stored on the node. + +| Category | Kind | Modality of claim | Source question | +| --- | --- | --- | --- | +| basic | `goal` | Value or outcome claim | "What outcome are we after?" | +| basic | `thesis` | Position or bet claim | "What do we believe about who this is for and why?" | +| basic | `term` | Naming commitment | "What do we mean when we say X?" | +| basic | `context` | Descriptive claim | "What is true about the world this lives in?" | +| structural | `requirement` | Obligation claim | "What must the system do?" | +| structural | `assumption` | Uncertainty claim | "What might be false?" | +| structural | `constraint` | Boundary claim | "What does this rule out?" | +| structural | `invariant` | Preservation claim | "What must never be broken?" | +| reasoning | `decision` | Choice claim | "What did we pick among real alternatives?" | +| reasoning | `criterion` | Oracle claim | "How will we judge that it holds?" | +| reasoning | `example` | Witness or disambiguator claim | "What concrete case would settle this?" | + +11 intent kinds, 3 derived categories. + +The **modality of claim** and **source question** columns are +agent-facing prompting guidance: they help the agent discriminate +between kinds when authoring nodes and help the elicitor choose +which question to ask next. The source question is the abstract +driver — it is not a literal question to parrot, but a heuristic +for what kind of material the node captures. + +**Category semantics:** + +- **`basic`** — grounding material. Establishes what/who/why before + structural elicitation can proceed. The spec-grade gate from + `grounding_onboarding` toward `elicitation_ready` requires a + satisficing threshold of `basic`-category nodes. The gate is + LLM-judged with a count floor — the agent assesses readiness, + but cannot declare grounding complete with zero basic nodes. + Grounding rubric (Walter-style questions: what is it, who is it + for, what problem, what value, when used, how measured) lives in + the prompt as abstract drivers, not structural enforcement. +- **`structural`** — core specification material. Requirements, + assumptions, and constraints form the structural backbone. +- **`reasoning`** — decisions, criteria, and evidence. Emerges as + the agent and user reason about structural material. + +### Oracle plane + +| Kind | Description | +| --- | --- | +| `check` | A verification action that witnesses or refutes a claim | +| `validation_method` | How a check is executed (unit test, manual, etc.) | +| `evidence` | A concrete artifact or observation produced by a check | +| `obligation` | A verification commitment — what must be checked | + +### Design plane + +| Kind | Description | +| --- | --- | +| `module` | A software component or subsystem | +| `interface` | A contract between modules | + +### Plan plane + +| Kind | Description | +| --- | --- | +| `milestone` | A bounded phase of work | +| `frontier` | A named canonical work item within a milestone | +| `slice` | A thin vertical implementation unit within a frontier | + +## Per-kind detail schemas + +Most kinds use `title` + `body` only. Two kinds have structured +`detail` sub-schemas validated by the `CommandExecutor`: + +```ts +// decision: REQUIRED detail +interface DecisionDetail { + readonly chosen_option: string + readonly rejected: string[] + readonly rationale: string +} + +// term: REQUIRED detail +interface TermDetail { + readonly definition: string + readonly aliases?: string[] +} +``` + +**Validation rules:** + +- `decision` and `term` nodes REQUIRE `detail`; the CommandExecutor + rejects creation without it. +- All other kinds: `detail` must be absent or null. +- Unknown fields in `detail` are rejected (closed validation). +- `detail` is stored as a JSON column in SQLite — one `nodes` + table for all planes and kinds. + +## Prompting guidance for kind discrimination + +The modality-of-claim table (§Intent plane) is the primary agent +rubric. Additional prompting heuristics for kinds that need them: + +- **`requirement` duality.** A requirement may be user-story-shaped + (stated directly by a stakeholder, `source: "stakeholder"`, + `basis: "explicit"`) or projection-shaped (derived from existing + goals/theses/constraints via `project-graph`, `source: "derived"`, + `basis: "accepted_review_set"`). Both are obligation claims. The + `source` and `basis` fields carry the provenance distinction; + strategy prompt packs (`step-wise` vs `project-graph`) guide the + agent on which framing to use. +- **`decision` capture criteria.** A claim should become a + `decision` only if all of the following hold: + 1. **Plausible alternatives existed** — "we chose A over B" + 2. **The choice is durable** — it constrains future work + 3. **The choice is explicit** — stated, not implied + 4. **Rejected alternatives can be named** — at least one + 5. **There is a rationale** — "because X" + + The `CommandExecutor` enforces `rejected.length >= 1` in + `DecisionDetail`. If none of these criteria hold, the material + is probably `context`, `requirement`, or `assumption` — not a + decision. +- **`invariant` vs `constraint`.** A constraint says "don't go + there" — it bounds the solution space. An invariant says "this + must always hold" — things break if it's violated. Constraints + get `boundary` edges; invariants get `dependency` and `proof` + edges. If invalidating the source should cascade downstream + breakage, it is an invariant; if it merely narrows what's in + scope, it is a constraint. +- **`thesis` carries the grounding material** that a prose spec + invests in: who this is for, what problem it solves, what value + it creates, what bet we're making. It is not a requirement (a + bet, not a need), not a goal (falsifiable, not aspirational), + and not an assumption (a chosen position, not a dependency). +- **`context` promotion heuristic.** Context is the last-resort + descriptive bucket — before filing a node as `context`, check + whether it should be promoted: + + | If the context… | Promote to… | + | --- | --- | + | must be true for success | `requirement` or `invariant` | + | limits acceptable solutions | `constraint` | + | may be false and matters | `assumption` | + | chooses among alternatives | `decision` | + | is a bet about users/market/value | `thesis` | + | just helps interpretation | keep as `context` | + +### Beyond the schema contract + +Two categories of agent-facing guidance live outside this document +because they evolve faster than the schema: + +- **Observer classification / translation tables** — phrase-pattern + → kind mappings for post-exchange capture. Seeded in + [`src/agents/strategies/README.md`](../../src/agents/strategies/README.md); + lands as prompt-pack content with M5 `agent-graph-integration`. +- **Topology-driven question ranking** — graph-shape heuristics + for what to ask next (e.g. "requirement with no incoming proof + edge → suggest a criterion"). Seeded in + [`src/agents/lenses/README.md`](../../src/agents/lenses/README.md); + lands as lens prompt-pack content with M5. + +Both draw on the archived +`/brunch/docs/design/INTENT_GRAPH_SEMANTICS.md` as source material. + +## `framing_as` — retired + +The prior `framing_as` orthogonal modality (problem, persona, JTBD, +non-goal, etc.) is retired. Its work is absorbed by: + +- **`thesis`** — carries "what/who/why" material (problem framing, + persona framing, value proposition framing) +- **`term`** — carries naming commitments +- **`constraint`** — carries exclusions and boundary claims +- **`invariant`** — carries preservation claims (was formerly + conflated with constraints) +- **`goal`** — carries aspirational intent + +The allowed `framing_as` matrix (I7-L) and the "promote when a +framing demands unique relation policy" escape hatch are both +retired. No node carries a `framing_as` field. + +## Open questions + +- **Tuple-label table location.** Likely + `src/graph/projection/labels.ts`; lands with the first + projection-builder slice in M4 or M5. +- **Realization watch criterion.** If probe runs surface three or + more realization sub-clusters that demand distinct cascade or + projection policy, split `realization` into siblings (probable + candidates: `implementation`, `establishment`, `assertion`, + `expression`). +- **Multi-edge between same pair.** The system-prompt discipline + says "use one edge, the strongest operational role." Whether to + also enforce a structural uniqueness constraint on + `(sourceId, targetId)` or `(sourceId, targetId, category)` is + deferred — the discipline plus the cost of false positives argue + against enforcement. +- **Recon_need closure on edge deletion.** If an edge is deleted, + any `ReconciliationNeed` with `target.kind = "edge"` referencing + it needs an explicit resolution rule. Likely: mark resolved with + reason `target_removed`. Deferred to the recon_need substrate + slice. + +## Supersession notes + +This document supersedes: + +- `docs/architecture/pi-seam-extensions.md` §"Edge types" (the + earlier M4 edge-type catalogue: `validates`, `instance_of`, + `produces`, `discharges`, `depends_on`, `derived_from`, + `counterexample_for`, `witnesses`) +- `archive/docs/design/INTENT_GRAPH_SEMANTICS.md` §"Relations" and + §"Edge schema and epistemic metadata" (already archived prior to + this document; the edge-layer content is now canonically here) +- `archive/docs/design/GRAPH_EDGE_CATEGORIES.md` — the brainstorm + that produced this document +- The `framing_as` orthogonal modality and allowed matrix from + `memory/SPEC.md` D7-L, A7-L, I7-L — absorbed by `thesis`, + `term`, `constraint.subtype`, and `goal` +- `EdgeProvenance` / node provenance — retired; `change_log` owns + audit trail + +Outbound references updated with Phase 2 lock: + +- `memory/SPEC.md` — D54-L (node shape), D55-L (provenance + retirement), D56-L (intent kind categories), D57-L (grounding + gate); A7-L retired; I7-L retired; I36-L, I37-L added +- `memory/PLAN.md` — `sealed-pi-profile-runtime-state` Phase 2 + node lock acceptance criteria updated diff --git a/docs/design/REVIEW_SETS.md b/docs/design/REVIEW_SETS.md index e32abeb5f..71a2c9a5e 100644 --- a/docs/design/REVIEW_SETS.md +++ b/docs/design/REVIEW_SETS.md @@ -22,6 +22,11 @@ This pattern is **reusable across generative lenses**: the same mechanism that h Generative-lens proposals carry **structured entity-draft payloads** in the proposal custom entry. The proposal contains the graph entities and edges that *would* be created on acceptance, in a form `CommandExecutor` can validate without re-parsing. +Edge drafts follow the locked graph contract from [GRAPH_MODEL.md](GRAPH_MODEL.md): +closed `category` values, optional `stance` only for `proof`/`support`, and +`basis: "accepted_review_set"` for proposal-time edges. Review-set payloads no +longer carry a free-form `relation` string. + Approximate shape (refined during M5 implementation): ```text @@ -43,7 +48,13 @@ Approximate shape (refined during M5 implementation): ... ], edge_drafts: [ - { from_draft_id, to_draft_id, relation }, + { + category: "support", + source_draft_id: "persona-1", + target_draft_id: "requirement-2", + stance: "for", + basis: "accepted_review_set", + }, ... ], rubric: { diff --git a/memory/PLAN.md b/memory/PLAN.md index 7c5117b47..015259cdf 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -14,7 +14,11 @@ ## Context -Brunch-next is proceeding on the razed `next` line (tag `next-baseline`) as a thin product layer over `pi-coding-agent`. M0–M3 proved the basic host, JSONL transcript viability, probe/RPC substrate, and read-only web shell; detailed completed frontier definitions now live in `docs/archive/PLAN_HISTORY.md`. The remaining FE-744 work is Pi-wrapping closeout, not public-RPC substrate doubt: raw Pi RPC editor fallback, public Brunch JSON-RPC assistant-first structured-exchange permutation parity, web real-time observation of RPC-originated structured-exchange updates, private Brunch prompt-pack topology, and the Zod-authored structured-exchange details schema layer have landed. FE-744's visual chrome closeout has landed; strict command containment remains recorded as an A18-L residual Pi API risk requiring a Pi command/keybinding policy seam rather than more Brunch wrapper work. After FE-744, `sealed-pi-profile-runtime-state` must make the embedded Pi harness product-safe. In concrete terms, the sealed-profile/runtime-state frontier prevents ambient user/project `.pi/` settings or resources from shaping Brunch behavior, and persists the active operational mode, role preset/runtime bundle, strategy, and lens in the linear transcript so prompt/tool posture can be reconstructed at turn boundaries. The M4 graph data plane remains structurally next after those harness/control-plane risks are scoped. +Brunch-next is proceeding on the razed `next` line (tag `next-baseline`) as a thin product layer over `pi-coding-agent`. M0–M3 plus `pi-ui-extension-patterns` (FE-744) proved the basic host, JSONL transcript viability, probe/RPC substrate, read-only web shell, Pi extension seams, and public-RPC structured-exchange parity; detailed completed frontier definitions live in `docs/archive/PLAN_HISTORY.md`. The active frontier is now `sealed-pi-profile-runtime-state`, expanded in place into a **prep envelope before `graph-data-plane` (M4) CRUD**. It carries two strands under one branch (`ln/fe-776-graph-layer-prep-profile`): **(a) Pi harness sealing** — Brunch-owned programmatic settings/resource/tool/prompt/keybinding policy isolates product behavior from ambient user/project `.pi/`, and operational-mode / role-preset / strategy / lens state is appended to Pi JSONL as Brunch custom entries reconstructed at turn boundaries; **(b) graph-model lock-and-materialize** — lock the conceptual edge and node contracts in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), stub the type/policy surface under `src/graph/`, and prove the A20-L Drizzle 1.0-beta + `drizzle-orm/typebox` + `better-sqlite3` + Pi `registerTool` round-trip so M4 CRUD lands on settled persistence/schema-derivation foundations. Phase 1 (edges) has landed; Phase 2 (nodes) and the Drizzle spike are the remaining moves before `graph-data-plane` resumes. A18-L strict command containment is still carried as a residual Pi API risk to route as a narrow upstream ask if the embedded-harness strand surfaces a clean seam. + +Architecture grill (2026-06-01) locked several decisions that shape graph-data-plane and agent-graph-integration: **(1)** source topology `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies (D52-L); **(2)** `commitGraph` — a single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references, one LSN, all-or-nothing (D53-L, I34-L); **(3)** the `propose-graph` strategy bypasses review-set — user accepts a concept, agent generates and persists the full subgraph through `commitGraph` directly (D26-L updated); **(4)** strategy/lens axis split — strategies are interaction shapes (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`), lenses are topical focus (`intent`, `design`, `oracle`) (D25-L updated). The `commitGraph` path under `propose-graph` is the primary A14-L proof target: if LLMs cannot produce structurally-legal multi-node multi-edge batches, the core flow must be re-architected. + +Phase 2 node grill (2026-06-01) locked the node layer: **(1)** common flat `GraphNode` shape with `title`, `body`, `basis`, `source` (free-form epistemic attribution), and `detail` JSON column (D54-L); **(2)** `provenance` retired from both nodes and edges — `change_log` owns audit trail (D55-L); **(3)** 11 intent kinds in 3 derived categories: basic (`goal`, `thesis`, `term`, `context`), structural (`requirement`, `assumption`, `constraint`, `invariant`), reasoning (`decision`, `criterion`, `example`) (D56-L); **(4)** `framing_as` retired, absorbed by thesis/term/constraint/invariant/goal (A7-L retired, D7-L retired, I7-L retired); **(5)** spec-grade grounding gate: LLM-judged satisficiency with count floor on basic-category nodes, Walter-style rubric in prompt (D57-L); **(6)** `posture` is spec-level, not a graph node; **(7)** modality-of-claim + source-question rubric and context promotion heuristic for agent prompting. ### POC assumption pressure @@ -27,18 +31,18 @@ The POC should maximize assumption falsification rather than merely implement mi | A4-L global LSN adequacy | Replay, staleness, or reconciliation ordering needs per-entity/vector clocks. | `graph-data-plane` establishes one-LSN-per-transaction; `turn-boundary-reconciliation` tries to break it with cross-session traces. | | A5-L probe/transcript driver quality | Agent-as-user probes fail to catch regressions or cannot produce reviewable transcript evidence for realistic Brunch seams. | FE-744 has proved a deterministic public-RPC structured-exchange permutation driver; future brief-based or generative golden runs must pass through the `.fixtures/runs///` probe/transcript artifact path. | | A6-L unified `graph.*` namespace | Intent/oracle/design/plan semantics become confusing or unsafe under one umbrella. | `graph-data-plane` and `agent-graph-integration` should start unified but watch for namespace pressure. | -| A7-L `framing_as` modality | Product framings need relation policies that base kinds cannot express. | M4 schema plus targeted probe scenarios exercise framing; promote only if probe evidence demands it. | +| A7-L `framing_as` modality | ~~Product framings need a node-shape carrier that `framing_as` cannot express.~~ | **Retired.** Phase 2 node lock absorbed `framing_as` into first-class `thesis`, `term`, `context`, `constraint`, `invariant`, and `goal` kinds (D54-L, D56-L). | | A8-L reconciliation substrate | Gaps, contradictions, process debt, and conflicts need separate substrates immediately. | `graph-data-plane` builds the shared substrate; `coherence-first-class` and known-bad briefs test subtype pressure. | | A9-L mention ledger granularity | Session-scoped snapshots miss necessary staleness or create noisy hints. | Defer until `turn-boundary-reconciliation`, after graph ids/LSNs exist. | | A11-L next-turn delivery | Side-task/reviewer results require mid-turn delivery or another event plane. | Keep deferred until M5/M7 side-task/reviewer paths exist; test at turn-boundary rendezvous. | | A13-L deferred observer/auditor queue | Async audit/backfill needs canonical chat/turn tables or privileged writes. | Not load-bearing after D18-L; defer until a backstop queue is actually introduced. | -| A14-L review-set structural legality | LLMs cannot produce dry-run-valid entity/edge drafts reliably enough. | M5 must measure structural-legality rate and retry/fallback behavior before depending on proposal-heavy UX. | +| A14-L graph-mutation structural legality | LLMs cannot produce structurally-legal `commitGraph` batches (multi-node multi-edge with intra-batch refs) or review-set entity drafts reliably enough. The `propose-graph` → `commitGraph` path (D53-L) is the primary proof target. | `graph-data-plane` must land `commitGraph` batch validation (I34-L); `agent-graph-integration` must test LLM generation against real CommandExecutor. This is the highest-stakes assumption: failure requires re-architecture of the propose-graph flow. | | A15-L establishment hints | Offers are not reconstructable or useful from transcript entries alone. | M5 establishment-offer probe runs and FE-744 chrome affordances exercise this. | | A16-L reviewer trigger/scope | Reviewer findings are too slow, noisy, or incomplete under deferred policy. | Do not overbuild early; first accepted review-set probe runs should make reviewer policy empirical. | | A17-L elicitation temperament preference | Users do not need persistent interrogative/proposal preference. | Outer-loop adoption signal only; do not block POC. | | A18-L command containment | Hiding suggestions + lifecycle blocking leaves unsafe Pi built-ins reachable. | FE-744 product-shell evidence must name any Pi upstream seam before M5/M6 authority work relies on it. | | A19-L sealed Pi profile | Ambient `.pi` settings/resources still shape Brunch product behavior. | `sealed-pi-profile-runtime-state` is a gate before graph tools and authority-sensitive agent work. | -| A20-L Drizzle 1.0 beta | Beta blocks migrations, SQLite fidelity, or TypeBox derivation. | `graph-data-plane` starts with a version/schema spike before broad imports. | +| A20-L Drizzle line + schema path | The chosen Drizzle line blocks migrations, SQLite fidelity, monotonic counter/change-log mechanics, or runtime-schema derivation. | Prove the persistence seam now inside `sealed-pi-profile-runtime-state`: one representative table plus monotonic counter / change-log skeleton, then let `graph-data-plane` inherit the settled choice. | | A21-L bounded coherence | Contradiction/gap verdicts cannot represent useful coherence without broader judgment. | Keep implementation late (M8), but design known-bad probe scenarios earlier so the rubric is falsifiable. | | A22-L synchronous elicitor capture | Elicitor over-captures, misses obvious facts, or cannot use preface to resolve uncertainty. | `agent-graph-integration` needs targeted capture probe runs before async observer backstops are reconsidered. | @@ -47,13 +51,11 @@ The POC should maximize assumption falsification rather than merely implement mi ### Active -1. `pi-ui-extension-patterns` — FE-744 Pi-wrapping proof is closing: raw Pi RPC editor fallback, public Brunch JSON-RPC structured-exchange permutation parity, web real-time observation of RPC-originated structured-exchange updates, private prompt-pack topology, structured-exchange schema layer, and branded/themed chrome recovery have landed. Remaining work is PR/branch tie-off plus carrying A18-L strict command-containment risk forward to the sealed-profile/runtime-state or upstream-Pi seam, not more FE-744 implementation. +(none — `graph-data-plane` just completed; `agent-graph-integration` is next) ### Next -1. `sealed-pi-profile-runtime-state` — Seal Brunch's embedded Pi profile and transcript-backed runtime-bundle state before future agent-loop work depends on ambient-safe settings, prompt composition, or tool gating. -2. `graph-data-plane` — M4 remains structurally next after FE-744 chrome closeout and the sealed-profile/runtime-state follow-up are scoped; the public-RPC elicitation input loop is no longer the blocker. -3. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, and reviewer advisory writes through pi extension seams; all writes via the shared command layer. +1. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, and reviewer advisory writes through pi extension seams; all writes via the shared command layer. ### Parallel / Low-conflict @@ -75,42 +77,48 @@ The POC should maximize assumption falsification rather than merely implement mi ### sealed-pi-profile-runtime-state -- **Name:** Sealed Pi profile and transcript-backed runtime state -- **Linear:** unassigned -- **Kind:** structural hardening -- **Status:** not-started -- **Objective:** Turn the discussion-locked Brunch Pi Profile and runtime-bundle model into code/tests by porting the useful `.pi/` probe extensions into explicit Brunch-owned product modules under `src/tui-client/.pi/extensions/*` plus aggregate `src/tui-client/pi-extension-shell.ts`: Brunch-owned programmatic settings/resource/tool/prompt/keybinding policy isolates product behavior from ambient user/project `.pi/`; operational mode / role preset / strategy / lens state is appended to Pi JSONL as Brunch custom entries and reconstructed at turn boundaries. -- **Why now / unlocks:** FE-744 proved multiple Pi extension seams and exposed the exact weak point: ambient resource discovery is mostly disabled, but `SettingsManager.create(cwd, agentDir)` can still leak behavior-shaping settings, and future `elicit` vs `execute` work needs prompt/tool posture to be stateful without hidden extension memory. This frontier de-risks M5/M6/M7 before graph tools, capture/reviewer jobs, and authority gating depend on the embedded harness. -- **Acceptance:** A `BrunchPiProfile` (or equivalent module boundary) owns settings policy, resource-loader options, extension factories, keybinding/command policy, tool policy, and prompt policy; tests prove ambient context files/extensions/skills/prompt templates/themes do not load while explicit Brunch-owned extension-discovered resources can load intentionally through Pi `resources_discover`; settings that affect product behavior are overridden/sealed or documented as a Pi upstream seam; runtime extension factories now load explicitly from `src/tui-client/pi-extension-shell.ts` / `src/tui-client/.pi/extensions/*` and reusable TUI components under `src/tui-client/.pi/components/*`, with no root project-local Pi discovery path as product runtime. Full selected-state transcript entries under `brunch.agent_runtime_state` can be appended by Brunch helpers and replayed to reconstruct active operational mode, role preset/runtime bundle, strategy, and lens; turn prep composes prompt packs from base Brunch prompt + operational mode + role preset + strategy + lens + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules; `elicit` suppresses execute/dangerous tools such as raw `bash`/`write` unless explicitly allowed by the active bundle. -- **Verification:** Inner — profile/runtimestate unit tests, prompt-composition snapshot tests, and tool-policy contract tests. Middle — ambient `.pi/` fixture/audit tests proving disabled discovery and sealed settings; explicit Brunch resource-injection test proving extension factories may inject Brunch-owned skills/prompts despite ambient `noSkills`/`noPromptTemplates`; JSONL reload/projection tests for runtime init/switch entries; before-agent-start/tool-call policy tests for `elicit`. Outer — manual TUI/RPC smoke that active role/lens/strategy changes are inspectable in transcript and reflected in prompt/tool posture rather than hidden UI state. -- **Cross-cutting obligations:** Do not expose Pi's generic extension/skill/prompt/theme configuration to Brunch users; do not make Pi skills the primary authority for core operational prompts; keep raw Pi RPC behind Brunch adapters; keep runtime state linear-transcript-backed and compatible with compaction/session-boundary lifecycle hooks (`session_start`, `resources_discover`, `before_agent_start`, `context`, `tool_call`, `session_before_switch`, `session_before_compact`, `session_shutdown`). -- **Traceability:** R25, R26 / D2-L, D23-L, D39-L, D40-L / I24-L, I25-L / A19-L -- **Design docs:** [pi-seam-extensions.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md), [pi-ui-extension-patterns.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-ui-extension-patterns.md) -- **Current execution pointer:** do not start this frontier until FE-744 closes the remaining product-surface relay and chrome-recovery seams. Then scope the profile audit first: preserve current `noContextFiles`/`noExtensions`/`noPromptTemplates`/`noSkills`/`noThemes` posture, prove extension-factory resource injection is intentional, and seal or document the remaining `SettingsManager` leakage. Follow-up slices should add any best-effort lifecycle-generated session display names over Pi `session_info` and tighten prompt/tool policy around transcript-backed runtime bundles. +- **Name:** Sealed Pi profile, transcript-backed runtime state, and graph-model prep (M4 prep envelope) +- **Linear:** [FE-776](https://linear.app/hash/issue/FE-776) +- **Branch:** `ln/fe-776-graph-layer-prep-profile` +- **Kind:** structural hardening (prep envelope before M4 CRUD) +- **Status:** done +- **Objective:** Broader prep envelope before `graph-data-plane` (M4) CRUD work begins. Two strands under one frontier/branch: **(a) Pi harness sealing** — Brunch-owned programmatic settings/resource/tool/prompt/keybinding policy isolates product behavior from ambient user/project `.pi/`; operational mode / role preset / strategy / lens state is appended to Pi JSONL as Brunch custom entries and reconstructed at turn boundaries. **(b) Graph-model lock-and-materialize** — lock the conceptual edge and node contracts in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), stub the type/policy surface under `src/graph/`, and prove the A20-L persistence seam now: the Drizzle line, row-schema derivation path, monotonic counter allocation, change-log shape, and Pi `registerTool` round-trip so M4 CRUD lands on settled persistence/schema-derivation foundations. +- **Why now / unlocks:** FE-744 proved multiple Pi extension seams and exposed the exact weak point: ambient resource discovery and settings policy had to be sealed before future `elicit` vs `execute` work could depend on product-owned prompt/tool posture. Once sealing is in code, the cheapest remaining moves before M4 CRUD are conceptual rather than persistence-mechanical: lock the graph model so review-set drafts, reconciliation needs, snapshot bucketing, and CommandExecutor result discriminants share a stable contract; retire the speculative named-relation catalogue and brainstormed edge taxonomy; settle the persistence/schema-derivation toolchain (A20-L). De-risks M5/M6/M7 before graph tools, capture/reviewer jobs, and authority gating depend on the embedded harness or the graph data plane. +- **Acceptance:** + - **Sealing strand:** A `BrunchPiProfile` (or equivalent module boundary) owns settings policy, resource-loader options, extension factories, keybinding/command policy, tool policy, and prompt policy; tests prove ambient context files/extensions/skills/prompt templates/themes do not load while explicit Brunch-owned extension-discovered resources can load intentionally through Pi `resources_discover`; settings that affect product behavior are overridden/sealed or documented as a Pi upstream seam; runtime extension factories load explicitly from `src/tui-client/pi-extension-shell.ts` / `src/tui-client/.pi/extensions/*` and reusable TUI components under `src/tui-client/.pi/components/*`, with no root project-local Pi discovery path as product runtime. Full selected-state transcript entries under `brunch.agent_runtime_state` can be appended by Brunch helpers and replayed to reconstruct active operational mode, role preset/runtime bundle, strategy, and lens; turn prep composes prompt packs from base Brunch prompt + operational mode + role preset + strategy + lens + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules; `elicit` suppresses execute/dangerous tools such as raw `bash`/`write` unless explicitly allowed by the active bundle. + - **Graph-model strand:** Phase 1 edge contract locked in `docs/design/GRAPH_MODEL.md` and materialized as type/policy stubs under `src/graph/` (✓ landed at commit `100585a1`). Phase 2 node contract locked (✓ landed at commits `b6ecec1e`–`8346e23d`): 11 intent kinds in 3 derived categories (basic/structural/reasoning), common `GraphNode` shape with `detail` JSON column for `decision`/`term`, `provenance` retired from both nodes and edges, `framing_as` retired, `source` as free-form epistemic attribution, modality-of-claim + source-question agent rubric, context promotion heuristic. Materialized as `src/graph/schema/nodes.ts` and reflected in SPEC via D54-L/D55-L/D56-L/D57-L plus I36-L/I37-L. A20-L spike produces a verdict on the persistence seam over one representative intent-plane slice: Drizzle line choice, row-schema derivation path (`drizzle-zod`, `drizzle-orm/typebox`, or equivalent), monotonic counter allocation, change-log writes, and Pi `registerTool` round-trip. +- **Verification:** Inner — profile/runtimestate unit tests, prompt-composition snapshot tests, tool-policy contract tests, edge/node schema unit tests, category-policy unit tests. Middle — ambient `.pi/` fixture/audit tests proving disabled discovery and sealed settings; explicit Brunch resource-injection test proving extension factories may inject Brunch-owned skills/prompts despite ambient `noSkills`/`noPromptTemplates`; JSONL reload/projection tests for runtime init/switch entries; before-agent-start/tool-call policy tests for `elicit`; persistence spike tests covering one representative table, one insert/select cycle, monotonic counter allocation, change-log append shape, and one Pi `registerTool` parameter binding; I26-L grep-based architectural test wired alongside the first Drizzle import so the single-schema-vocabulary boundary stays enforced. Outer — manual TUI/RPC smoke that active role/lens/strategy changes are inspectable in transcript and reflected in prompt/tool posture rather than hidden UI state. +- **Cross-cutting obligations:** Do not expose Pi's generic extension/skill/prompt/theme configuration to Brunch users; do not make Pi skills the primary authority for core operational prompts; keep raw Pi RPC behind Brunch adapters; keep runtime state linear-transcript-backed and compatible with compaction/session-boundary lifecycle hooks (`session_start`, `resources_discover`, `before_agent_start`, `context`, `tool_call`, `session_before_switch`, `session_before_compact`, `session_shutdown`). Graph-model lock work must trace to `docs/design/GRAPH_MODEL.md`; node lock must preserve the closed-edge-set invariants (immutable accepted-edge identity, `dependency`-only auto-cascade, separate `ReconciliationNeed` substrate with `{kind:'edge'|'node_pair'}` target). The persistence spike is throwaway scope — one representative slice, no broad imports until the verdict lands; if the current beta line blocks (migrations, SQLite fidelity, schema-derivation bugs, or ergonomics), pick the simpler working adapter/line and continue without re-opening M4 design. +- **Traceability:** R25, R26 / D2-L, D16-L, D23-L, D39-L, D40-L, D41-L, D51-L / I24-L, I25-L, I26-L / A19-L, A20-L +- **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (canonical graph contract; Phase 1 + Phase 2 locked), [pi-seam-extensions.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md), [pi-ui-extension-patterns.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-ui-extension-patterns.md) +- **Current execution pointer:** + - **Sealing strand:** ✓ Complete. Profile resource/settings/runtime slices landed. Session display names generated from spec title + ordinal, persisted via Pi `session_info`, rendered in TUI chrome. + - **Graph-model strand:** ✓ Complete. Phase 1 (edges) + Phase 2 (nodes) locked. A20-L persistence spike validated (`drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`). + - **Tie-off:** ✓ Both strands at acceptance. `graph-data-plane` (M4 CRUD) is unblocked. ### graph-data-plane -- **Name:** Graph data plane (intent-first, workspace-graph-ready) (M4) +- **Name:** Graph data plane (intent-first, workspace-graph-ready) (M4 CRUD) - **Linear:** [FE-741](https://linear.app/hash/issue/FE-741/graph-data-plane-intent-first-workspace-graph-ready-m4) -- **Branch:** `ln/fe-741-graph-data-plane` (stacked on `ln/fe-737-web-shell`) +- **Branch:** `ln/fe-741-graph-data-plane` (stacked on `ln/fe-737-web-shell`; will re-base above `ln/fe-776-graph-layer-prep-profile` once the prep envelope ties off) - **Kind:** structural -- **Status:** next / unblocked by FE-744 chrome recovery; paused until the sealed-profile/runtime-state follow-up is scoped -- **Objective:** Stand up SQLite-backed graph persistence; durable intent-plane nodes and edges; a single global LSN per commit; the change log; the reconciliation-need substrate; named homes for coherence state (verdicts and violations) — all forward-compatible with oracle, design, and plan planes. -- **Why now / unlocks:** Pins I1-L, I6-L. Unlocks all agent ↔ graph work (M5+) and lets oracle / design / plan planes be added later without re-foundation. -- **Acceptance:** Graph CRUD + change-log replay tests pass through the `CommandExecutor` public mutation boundary; command results already include success, `needs_human`, `policy_blocked`, `version_conflict`, and `structural_illegal` shapes even if pre-M6 policy classification is minimal; reconciliation-need substrate accepts inserts/updates/resolutions with LSN invariants enforced; oracle-plane stub tables exist (Check, Validation Method, Evidence, Obligation) even if unused; the persistence layer proves the one-transaction protocol that couples authority/result classification, version checks, structural validation, LSN allocation, change-log append, and any coherence updates. -- **Verification:** Inner gate plus command/result schema/type tests. Middle — property/model-based tests on LSN monotonicity, graph replay, reconciliation invariants, framing matrix, and `CommandExecutor` transaction/result behavior; architectural no-bypass tests. Outer — fixture property invariants on reconciliation-substrate begin running. -- **Cross-cutting obligations:** Establish the Drizzle + `better-sqlite3` persistence shape, `CommandExecutor` result contract, and no-bypass transaction rule as shared infrastructure for later direct-agent, elicitor-capture, deferred observer/auditor, side-task, migration, and UI-attributed writes. Derive row/insert/update runtime schemas from Drizzle table definitions via TypeBox (`drizzle-orm/typebox` if A20-L resolves to the Drizzle 1.0 beta line; standalone `drizzle-typebox` + `drizzle-orm/typebox-legacy` otherwise) — do not hand-author parallel row schemas. Land the I26-L grep-based architectural test alongside the first Drizzle import so the single-schema-vocabulary boundary stays enforced. -- **Traceability:** R7, R9, R13 / D3-L, D4-L, D6-L, D8-L, D9-L, D16-L, D20-L, D41-L / I1-L, I6-L, I7-L, I11-L, I26-L / A3-L, A4-L, A20-L -- **Design docs:** [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [pi-seam-extensions.md §Graph clock, §Reconciliation-need substrate, §Oracle plane](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) -- **Current execution pointer:** start by scoping the narrow `CommandExecutor` result contract and one-transaction LSN/change-log skeleton before widening CRUD or coherence homes. Pair the first slice with an A20-L spike (Drizzle 1.0 beta + `drizzle-orm/typebox` + `better-sqlite3` + Pi `registerTool` round-trip) so the version pin and schema-derivation path are settled before later slices import them broadly. Keep M4 thin enough to falsify A3-L/A4-L/A6-L/A8-L/A20-L before widening CRUD or coherence homes. +- **Status:** done (all 6 execution steps complete 2026-06-01) +- **Objective:** Stand up SQLite-backed CRUD over the prep-envelope-locked graph model: durable intent-plane nodes and edges per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md); a single global LSN per commit; the change log; the reconciliation-need substrate; named homes for coherence state (verdicts and violations); and the `commitGraph` atomic batch mutation (D53-L) that accepts `{ nodes, edges }` with intra-batch and existing-node references — all forward-compatible with oracle, design, and plan planes. Source topology follows D52-L: `db/` owns Drizzle schema and migrations; `graph/` owns the CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, and recon-need substrate; `graph/` imports from `db/`; no other layer imports `db/` directly. +- **Why now / unlocks:** Pins I1-L, I6-L. Unlocks all agent ↔ graph work (M5+) and lets oracle / design / plan planes be added later without re-foundation. The graph contract and persistence toolchain are settled by the prep envelope, so M4 is pure CRUD/transaction/CommandExecutor work rather than mixed design-and-mechanics. Landing `commitGraph` here (not deferring to M5) means the A14-L proof can run as soon as agent tools are wired. +- **Acceptance:** Graph CRUD + change-log replay tests pass through the `CommandExecutor` public mutation boundary; command results already include success, `needs_human`, `policy_blocked`, `version_conflict`, and `structural_illegal` shapes even if pre-M6 policy classification is minimal; `commitGraph` batch validation is all-or-nothing (I34-L) — if any node or edge fails structural checks, the entire batch is rejected with diagnostics sufficient for agent self-correction; reconciliation-need substrate accepts inserts/updates/resolutions with LSN invariants enforced; oracle-plane stub tables exist (Check, Validation Method, Evidence, Obligation) even if unused; graph snapshot readers support at least two detail levels (cursory full-graph overview, node-neighborhood with hops) per I35-L; the persistence layer proves the one-transaction protocol that couples authority/result classification, version checks, structural validation (including the closed edge-category set, immutable accepted-edge identity per D51-L, and intra-batch reference resolution per D53-L), LSN allocation, change-log append, and any coherence updates. +- **Verification:** Inner gate plus command/result schema/type tests. Middle — property/model-based tests on LSN monotonicity, graph replay, reconciliation invariants (target shape `{kind:'edge'|'node_pair'}`), framing matrix, edge structural legality (closed category set, stance scoping, supersession acyclicity), `commitGraph` batch all-or-nothing property (I34-L), intra-batch reference resolution correctness, existing-node reference validation, and `CommandExecutor` transaction/result behavior; architectural no-bypass tests. Outer — fixture property invariants on reconciliation-substrate begin running. +- **Cross-cutting obligations:** Reuse the Drizzle + `better-sqlite3` persistence shape settled by the prep-envelope A20-L spike; do not re-open the line/adapter choice in M4 unless the spike itself falsifies it. `CommandExecutor` result contract and no-bypass transaction rule become shared infrastructure for later direct-agent, elicitor-capture, deferred observer/auditor, side-task, migration, and UI-attributed writes. Derive row/insert/update runtime schemas from Drizzle table definitions via the schema path chosen during the spike — do not hand-author parallel row schemas. The I26-L grep-based architectural test should already be live from the prep envelope; M4 widens its coverage as new Drizzle imports land. `commitGraph` and `acceptReviewSet` are parallel paths to the same CommandExecutor — both must share the same validation, LSN, and change-log mechanics. +- **Traceability:** R7, R9, R13 / D3-L, D4-L, D6-L, D8-L, D9-L, D16-L, D20-L, D41-L, D51-L, D52-L, D53-L / I1-L, I6-L, I7-L, I11-L, I26-L, I34-L, I35-L / A3-L, A4-L, A14-L +- **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (canonical graph contract), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [pi-seam-extensions.md §Graph clock, §Reconciliation-need substrate, §Oracle plane](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) (note: §"Edge types" in pi-seam-extensions.md is retired and superseded by `docs/design/GRAPH_MODEL.md`) +- **Current execution pointer:** **(1)** ✓ `src/db/` Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** ✓ `CommandExecutor` result contract and one-transaction LSN/change-log skeleton with `createNode` proof-of-life, I26-L architectural boundary test, NodeId/EdgeId corrected to `number`. **(3)** skipped — subsumed by (4). **(4)** ✓ `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation (closed category set, stance scoping, self-loop rejection), I34-L all-or-nothing (edge failure rolls back nodes), one LSN per batch, one change_log entry per batch. **(5)** ✓ graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L); **(6)** ✓ reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with target validation, LSN invariants, and change_log; oracle-plane stub acceptance met by existing `createNode` + `ORACLE_KINDS`. **All M4 graph-data-plane steps complete.** ### agent-graph-integration - **Name:** Agent ↔ graph integration through the shared command layer (M5) -- **Linear:** unassigned +- **Linear:** [FE-785](https://linear.app/hash/issue/FE-785) - **Kind:** structural - **Status:** not-started -- **Objective:** Brunch installs graph tools through pi's extension seams; agent graph operations, elicitor post-exchange capture writes, reviewer-attributed advisory writes, review-set batch acceptances, spec readiness grade/posture updates, and the transcript-native establishment/intent-hint surfaces all route exclusively through the Brunch-owned command layer and shared event substrate; web, TUI, and agent all observe the same changes. +- **Objective:** Brunch installs graph tools through pi's extension seams; agent graph operations — including `commitGraph` batch mutations for the `propose-graph` direct-commit path (D53-L, D26-L) — elicitor post-exchange capture writes, reviewer-attributed advisory writes, review-set batch acceptances for `project-graph`, spec readiness grade/posture updates, and the transcript-native establishment/intent-hint surfaces all route exclusively through the Brunch-owned command layer and shared event substrate; web, TUI, and agent all observe the same changes. **The primary A14-L proof runs here:** test whether the LLM can produce structurally-legal `commitGraph` batches against the real CommandExecutor with bounded retry. - **Acceptance:** ```text @@ -143,7 +151,7 @@ The POC should maximize assumption falsification rather than merely implement mi └── async substrate (conditional) └── if observer/auditor queues land → backstops only, not primary capture freshness path ``` -- **Implementation layout:** Put the Pi-facing adapter in one explicit product extension directory, `src/tui-client/.pi/extensions/graph/`, imported by `src/tui-client/pi-extension-shell.ts` as `registerBrunchGraph` rather than discovered dynamically. Use `graph/index.ts` only to register Pi tools, message renderers, and event hooks. Keep tool definitions in `graph/tools/*` (`read-graph`, `create-intent-node`, `update-intent-node`, `link-intent-nodes`, `accept-review-set`), boundary schemas in `graph/schemas/*` (`tool-inputs`, `tool-results`, `custom-entries`), transcript helpers in `graph/transcript/*` (`entries`, `projections`, `renderers`), synchronous capture in `graph/capture/post-exchange-capture.ts`, reviewer target enforcement in `graph/reviewer/reviewer-writes.ts`, and the Pi→CommandExecutor translation seam in `graph/command-adapter.ts`. The extension directory must not own SQLite/Drizzle persistence, LSN allocation, structural graph validators, reviewer-agent implementation, or capture model/prompt machinery; those are Brunch product/core modules passed into the extension through explicit shell options such as `{ graph: { commandExecutor, capturePostExchange?, reviewerWrites? } }`. +- **Implementation layout:** Per D52-L, graph domain logic lives in `src/graph/` (CommandExecutor, readers, policy, validators, snapshot functions) and persistence in `src/db/`. The Pi-facing adapter goes in one explicit product extension directory, `src/.pi/extensions/graph/`, imported by `src/.pi/pi-extension-shell.ts` as `registerBrunchGraph` rather than discovered dynamically. Use `graph/index.ts` only to register Pi tools, message renderers, and event hooks. Keep tool definitions in `graph/tools/*` (`read-graph`, `commit-graph`, `create-intent-node`, `update-intent-node`, `link-intent-nodes`, `accept-review-set`), boundary schemas in `graph/schemas/*` (`tool-inputs`, `tool-results`, `custom-entries`), transcript helpers in `graph/transcript/*` (`entries`, `projections`, `renderers`), synchronous capture in `graph/capture/post-exchange-capture.ts`, reviewer target enforcement in `graph/reviewer/reviewer-writes.ts`, and the Pi→CommandExecutor translation seam in `graph/command-adapter.ts`. The extension directory must not own SQLite/Drizzle persistence, LSN allocation, structural graph validators, reviewer-agent implementation, or capture model/prompt machinery; those are Brunch product/core modules passed into the extension through explicit shell options such as `{ graph: { commandExecutor, capturePostExchange?, reviewerWrites? } }`. Agent prompts, strategy definitions (including `propose-graph` and `project-graph`), lens definitions, and context builders live in `src/agents/` per D52-L. - **Verification:** Inner — verify gate plus graph-tool/capture/reviewer command shape tests, proposal-entry schema validation (`brunch.review_set_proposal` must declare `epistemic_status` and support/grounding coverage), establishment-offer / elicitor-intent-hint schema validation (must declare `lens`), structured-exchange `preface` contract tests, and projection-helper tests for latest-offer lookup. Middle — `CommandExecutor` contract tests including `acceptReviewSet` discriminants and the rule that only dry-run-valid proposals become reviewable review sets, direct-DB no-bypass checks, extension-layout/import-boundary tests proving `src/tui-client/.pi/extensions/graph/**` reaches graph mutation only through `command-adapter.ts` and never imports Drizzle/SQLite directly, post-exchange capture fixtures distinguishing committed facts from preface-only implications, reviewer-job restart/idempotence tests keyed by batch-acceptance entry id, reviewer-write-target architectural boundary test (rejects non-`reconciliation_need` targets), `acceptReviewSet` batch-atomicity property tests (one LSN / one change-log entry; partial-batch impossible under mid-batch validation failure), `supersedes`-chain acyclicity property tests, lens-routing correctness property tests, differential test comparing dry-run validation at proposal time vs real-run validation at acceptance, and cross-surface projection checks. Outer — kernel-card-output coverage assertions begin landing through targeted probe runs; first batch-proposal probe (e.g. `propose-scenarios-with-tradeoffs`) replays through review cycle + acceptance; A14-L proposal structural-legality rate captured in probe metadata as POC-phase fitness (not merge gate); 1–2 known-bad coherence-problem probe scenarios exercise reviewer precision; side-task / elicitor-capture / reviewer-attributed writes remain indistinguishable from other writes at the command-layer boundary except for attribution and reviewer's narrow target. - **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. - **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L @@ -227,22 +235,6 @@ The POC should maximize assumption falsification rather than merely implement mi - **Traceability:** A5-L - **Design docs:** [probes-and-transcripts.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/probes-and-transcripts.md) -### pi-ui-extension-patterns - -- **Name:** Prove Pi extension patterns for Brunch UI affordances -- **Linear:** [FE-744](https://linear.app/hash/issue/FE-744/pi-ui-extension-patterns) -- **Branch:** `ln/fe-744-pi-ui-extension-patterns` (off `ln/fe-737-web-shell`, parallel to `ln/fe-741-graph-data-plane`) -- **Kind:** structural (spike-flavored) -- **Status:** implementation-complete / tie-off (command-containment, dynamic chrome semantics, hierarchical spec/session picker startup + in-session flow, RPC/headless initial-selection contract, pty startup oracle, centered branded overlay reuse, evidence-memo reconciliation, structured-exchange schema/builder, TUI/editor adapters, live Pi RPC editor fallback, response-side projection, option-selection comments, structured-exchange editor fallback, raw Pi RPC structured-exchange evaluator proof, discoverable structured-exchange extension source at `src/tui-client/.pi/extensions/structured-exchange/index.ts`, public Brunch RPC structured-exchange tuple parity through the current deterministic permutation set, parity hardening for distinct exchange ids, terminal non-answered statuses, option content/rationale, no repeated deterministic prompts, committed `.fixtures` public-RPC parity probe artifacts, web real-time observation of RPC-originated structured-exchange transcript updates, private code-composed Brunch prompt-pack topology under `src/tui-client/.pi/context/`, the Zod-authored structured-exchange schema layer under `src/tui-client/.pi/extensions/structured-exchange/schemas/`, shared Brunch identity primitives, branded persistent chrome, and Brunch-host branded startup pty evidence have landed. Strict built-in command suppression remains an A18-L Pi API residue.) -- **Objective:** Demonstrate the Pi extension seams and Brunch product RPC seams needed before M5/M6/M7 depend on them: product-named commands routed through Brunch handlers; effect blocking for unsupported branch/session flows; dynamic Brunch-owned chrome through one wrapper; Brunch-owned startup/session selection; structured elicitation where system/assistant-originated questions use Pi transcript truth and TUI/RPC adapters; and, now active, a public Brunch JSON-RPC structured-exchange loop where an agent-as-user discovers methods, activates workspace/spec/session, starts/resumes assistant-first elicitation, answers pending structured exchanges through Brunch methods, and leaves transcript/projection evidence for current exchange permutations comparable to a TUI session. -- **Acceptance:** `docs/architecture/pi-ui-extension-patterns.md` catalogs the evidence with verdicts (`proven` / `feasible-with-cost` / `requires-pi-change` / `not-feasible`), distinguishes strict command suppression from lifecycle effect blocking, records the minimum upstream Pi command/keybinding policy ask, and captures the RPC degradation profile for chrome/custom UI. Brunch code exposes a product-named extension entrypoint plus wrappers for chrome, command policy, session lifecycle binding, and `/brunch`; the centered spec/session picker supports an optional continue-last fast path plus hierarchical create-spec/resume-spec/create-session/resume-session decisions without UI-owned session mutation and is shared by startup plus in-session adapters; TUI startup runs a Brunch-owned pre-Pi gate before `InteractiveMode` so prior transcript rendering is opt-in rather than implicit; creating a new session lands in a binding-only session for the selected spec; chrome receives the activated session id instead of fabricating `unbound`; the startup no-resume pty oracle proves stale transcript text is absent before explicit activation. Public RPC structured-exchange parity is now covered: `rpc.discover` describes the supported Brunch JSON-RPC surface with method descriptions, param/result schemas, and examples; `workspace.selectionState` / `workspace.activate` let the driver enter a new workspace→spec→session without invoking TUI picker code; `session.startElicitation`, `session.pendingExchange`, and `elicitation.respond` expose an assistant-first pending-exchange lifecycle over Brunch methods, not raw Pi commands; the deterministic agent-as-user driver answers the current structured-exchange permutations through Brunch JSON-RPC only and reports blockers/frictions; the resulting Pi JSONL plus `session.transcriptDisplay` and `session.elicitationExchanges` projections preserve prompt/question/option content/rationale/answer/comment/mode/status artifacts at TUI-comparable quality. Web clients now receive real-time product update notifications for RPC-originated structured-exchange mutations and refetch canonical projection handlers rather than reading from a parallel view store. Branded/themed chrome has been recovered through shared identity primitives, persistent chrome wrapper updates, and a Brunch-host branded startup pty oracle; persistent activated chrome has only qualitative manual-polish debt remaining. -- **Verification:** Inner — verify gate plus unit tests for any extension wrappers added; coordinator inventory/activation tests for switch decisions; source/contract tests that switcher UI returns decisions rather than mutating sessions; schema tests for structured question result details and JSON-editor request/response parsing. Middle — probe oracles per affordance category (manual checklist + executable postcondition checker on chrome state, JSONL tool results/custom entries emitted, or command-result discriminants); contract tests for Brunch handler shapes (`rpc.discover`, picker selection, elicitation start/pending/respond relay, transcript projections); pty/ANSI-stripped startup oracle proving no prior transcript appears before an explicit resume/open decision; raw Pi RPC probe demonstrating `ctx.ui.editor` JSON fallback round-trips through the documented extension UI protocol as supporting evidence only; scripted TUI demo covering all supported structured-exchange permutations; deterministic public Brunch RPC agent-as-user parity probe where the evaluator has a mission/intention, critical UX or feature-evaluation focus, permutation-bounded turn budget, and blocker/friction report; parity oracle over the saved Pi JSONL plus transcript/exchange projections, including no repeated deterministic prompts; web real-time update smoke proving browser state changes when selected session/exchange state changes via RPC-originated structured-exchange mutations; TUI-originated observation remains covered only if it reuses the same product invalidation path. Outer — manual TUI walkthrough validating visual quality, full-screen startup feel, interaction feel, and controllability cost between scripted-driver and manual paths. -- **Cross-cutting obligations:** Preserve the linear-transcript invariant (`I19-L`) — affordance prototypes must not introduce branch creation, mid-turn state mutations outside the command layer, or a parallel chat/turn store. Preserve the workspace hierarchy and startup invariant (`R19` / `I22-L`): the workspace is the cwd, not a user-created selectable object; `.brunch/state.json` is default acceleration, not implicit resume; no prior transcript or agent loop may run before an explicit spec/session activation decision. Spec/session picker UI must remain pure decision rendering; `WorkspaceSessionCoordinator` owns inventory, activation, state writes, session creation/opening, and binding. RPC/headless startup must expose structured initial-selection state/results, not invoke the TUI picker. Structured-exchange affordances must use Pi transcript truth first: `toolResult.details` may be the canonical structured response payload, including optional user `comment` fields for option-selection exchanges, while assistant tool-call args are positional/causal context. Slash commands and action buttons must route writes through the `CommandExecutor`; the JSON-editor RPC fallback is an adapter over Pi's supported extension UI protocol, not a new public Pi command family and not a bypass around Brunch's product RPC surface. Public agent-as-user probes must speak Brunch JSON-RPC (`rpc.discover`, `workspace.*`, `session.*`, `elicitation.*`) and may delegate to Pi RPC only behind Brunch adapters. Any new custom-entry kinds must declare `lens` per `I18-L` if elicitor-emitted. Establishment-offer affordances must stay orientation-first and user-invoked when expanded, rather than turning the full offer tree into a default next-action menu. TUI chrome/status affordances should call Brunch product wrappers rather than raw Pi `ctx.ui.*` primitives; the chrome wrapper must not publish its own `brunch.chrome` status key, and RPC fixtures should assert only chrome events that Pi actually emits for the current wrapper (diagnostic string-array `setWidget`, `setTitle`, notifications, and any future explicit status adapter rather than TUI-only header/footer). -- **Why now / unlocks:** Lens/review-set/reviewer UX in M5 and authority gating in M6 both assume Brunch can render rich interactive affordances over Pi without forking it. Proving the affordance set early de-risks those frontiers and lets the agent-as-user-driver extension question (controllability vs cost trade flagged in `ln-oracles` pass) be answered with evidence rather than estimation. Can run in parallel with `graph-data-plane` because TUI seams are independent of graph persistence. -- **Traceability:** R4, R14, R16, R17, R19, R20, R21, R24, R27, R28 / D2-L, D5-L, D11-L, D12-L, D13-L, D17-L, D19-L, D21-L, D22-L, D24-L, D25-L, D26-L, D27-L, D29-L, D32-L, D33-L, D34-L, D35-L, D36-L, D37-L, D38-L, D39-L, D40-L, D41-L, D48-L, D49-L, D50-L / I10-L, I13-L, I18-L, I19-L, I22-L, I23-L, I24-L, I25-L, I26-L, I32-L, I33-L / A14-L, A17-L, A18-L, A19-L -- **Design docs:** [pi-seam-extensions.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md), [pi-ui-extension-patterns.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-ui-extension-patterns.md), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md). -- **Current execution pointer:** FE-744 has landed the public-RPC structured-exchange parity spine and its hardening: `rpc.discover` lists the current Brunch methods; activated sessions can start/resume deterministic `present_*` pending exchanges; `elicitation.respond` appends matching `request_answer`, `request_choice`, or `request_choices` toolResult evidence; `session.pendingExchange`, `session.elicitationExchanges`, and `session.transcriptDisplay` project tuple-shaped Pi JSONL; `src/probes/public-rpc-parity-proof.ts` drives the deterministic permutation set through public Brunch JSON-RPC only; and the committed run under `.fixtures/runs/public-rpc-parity/2026-05-29-public-rpc-parity/` carries `session.jsonl`, rendered `transcript.md`, and `report.json`. The structured-exchange UI extension has been remodeled into sequential `present_*` / `request_*` tools under `src/tui-client/.pi/extensions/structured-exchange/`: `present_question`, `present_options`, `request_answer`, `request_choice`, and `request_choices` are registered; review/candidate tools remain named stubs and intentionally unregistered, while future `capture_*` tools are specified as transcript-native ANALYSIS toolResults. The Zod-authored schema layer under `src/tui-client/.pi/extensions/structured-exchange/schemas/` now captures the target present/request/capture details contract (`schema` + `v`, `exchange_id`, `tool_meta`, `comment`/`message`, candidate rubrics/graph refs, and minimal no-graph capture details) with parse and JSON Schema export tests; runtime tools and projections still use the existing tuple details model until a later migration slice deliberately rewires them to those exports. Pi can auto-discover the extension when launched from `src/tui-client` for `/reload`-based iteration, while production imports it explicitly through `src/tui-client/pi-extension-shell.ts`; keep tests under `src/tui-client/.pi/__tests__/`, not in auto-discovered `.pi/extensions` or `.pi/components` resource directories. The Brunch extension shell is explicit again, and Brunch product prompting now has a private prompt-pack/context topology at `src/tui-client/.pi/context/`; do not expose these packs through Pi `resources_discover`/`promptPaths`. Next build: scope `sealed-pi-profile-runtime-state`; do not return to `graph-data-plane` until the sealed-profile/runtime-state follow-up is scoped. A18-L strict command-containment is accepted as a residual Pi API risk unless that follow-up explicitly routes a narrow upstream/API ask. Run a separate `ln-design` pass before expanding capture-analysis payloads, shared transcript component subparts, or the runtime migration from tuple details to the new Zod exports. - ### flue-pattern-adoption - **Name:** Adopt selected Flue patterns post-POC @@ -292,29 +284,27 @@ The POC should maximize assumption falsification rather than merely implement mi ## Recently Completed -- 2026-05-22 `web-shell` — Done: M3 now serves the native React web shell over one persistent WebSocket RPC client, blocks/adjudicates branchy transcript shapes for session-consuming reads, serves only static HTTP assets (no REST product reads), projects explicit durable sessions through a canonical Brunch session-envelope reader, renders assistant/user/prompt transcript rows, and keeps browser state as a read-only client attachment rather than a durable session. Verified: `npm run verify` after each slice plus direct host/WebSocket smoke for static HTML, missing REST product reads, explicit `{ sessionId, specId }` projections, transcript display, and exchange projection. Accepted deferral: qualitative browser-open smoke remains environment-blocked by the current macOS sandbox. -- 2026-05-21 `jsonl-session-viability` — Done: Pi JSONL reload preserves coordinator-created binding-only sessions, first assistant/user flushes without duplicate prefixes, `/new` same-spec bindings, raw user/assistant payloads, representative Brunch custom entries, context-participating custom messages, continuity/compaction metadata, structured elicitation entries, defensive active-branch projection behavior, and M1 bundle-local replay parity for the now-retired scripted brief captures. Verified at the time with `npm run verify` after each slice; scripted captures were later deleted when tuple-shaped structured-exchange probes superseded the lightweight prompt/response fixture shape. Watch: M2 validates JSONL as sufficient for Brunch-supported linear sessions on current POC terms; branch-aware Brunch sessions are intentionally unsupported per D24-L, and later side-task, mention, and continuity frontiers still own their final payload semantics. -- 2026-05-21 `mode-shell-and-fixture-driver` — Done: print and RPC transport modes boot through the Brunch host; named `workspace.snapshot` and `session.elicitationExchanges` handlers project coordinator-selected session state; the first capture path copied the same selected Pi JSONL session projected by RPC and produced scripted brief captures. Those M1 artifacts and their milestone probe script are now retired: FE-744 tuple-shaped public-RPC probe artifacts are the current probe/transcript evidence. Historical verification included `npm run verify`, RPC/print parity smoke, exchange projection tests, fixture replay/projection parity tests, and human inspection. +- 2026-06-01 `graph-data-plane` (FE-741) — Done: all 6 execution steps complete. **(1)** Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `createNode` proof-of-life, I26-L architectural boundary test. **(3)** skipped (subsumed by 4). **(4)** `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation, I34-L all-or-nothing. **(5)** graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L). **(6)** reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with target validation + LSN invariants; oracle-plane stub acceptance met by existing node kinds. Verified: `npm run verify` after each slice. `agent-graph-integration` (M5) is now unblocked. +- 2026-06-01 `sealed-pi-profile-runtime-state` (FE-776) — Done: prep envelope tied off. Both strands complete: **(a)** Pi harness sealing including sealed profile, runtime-state transcript projection, session display names via Pi `session_info`; **(b)** graph-model lock-and-materialize with Phase 1 (edges) + Phase 2 (nodes) locked in `docs/design/GRAPH_MODEL.md`, code stubs under `src/graph/`, and A20-L persistence spike validating `drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`. `graph-data-plane` (M4 CRUD) is now unblocked. Verified: `npm run verify` after each slice. +- 2026-06-01 `pi-ui-extension-patterns` (FE-744) — Done. All Pi extension seam evidence for M5/M6/M7 landed. Detailed frontier definition archived to [docs/archive/PLAN_HISTORY.md §2026-06-01 Sync archive](file:///Users/lunelson/Code/hashintel/brunch-next/docs/archive/PLAN_HISTORY.md). -Older history: `docs/archive/PLAN_HISTORY.md` +Older history (including `web-shell`, `graph-data-plane` Phase 1 edge lock, `jsonl-session-viability`, `mode-shell-and-fixture-driver`, `walking-skeleton`): `docs/archive/PLAN_HISTORY.md` ## Dependencies ```text nodes: - pi-ui-extension-patterns [in-progress] - sealed-pi-profile-runtime-state [not-started] - graph-data-plane [paused] - agent-graph-integration [not-started] - subagents-for-proposal-diversity [deferred] - authority-model [not-started] - turn-boundary-reconciliation [not-started] - coherence-first-class [not-started] - compaction-and-conflict-widening [not-started] + sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) + graph-data-plane [done] (M4 CRUD proper) + agent-graph-integration [not-started] (M5) + subagents-for-proposal-diversity [deferred · optional] + authority-model [not-started] (M6) + turn-boundary-reconciliation [not-started] (M7) + coherence-first-class [not-started] (M8) + compaction-and-conflict-widening [not-started] (M9) probes-and-transcripts-evolution [continuous, parallel] edges: - pi-ui-extension-patterns -[hard]-> sealed-pi-profile-runtime-state sealed-pi-profile-runtime-state -[hard]-> graph-data-plane graph-data-plane -[hard]-> agent-graph-integration agent-graph-integration -[hard]-> authority-model @@ -335,4 +325,5 @@ notes: - probes-and-transcripts-evolution runs in parallel across all frontiers; not a spine edge. - unconnected items are horizon work; surfaced for acknowledgment, not active dependency. - the m5 -> subagents edge is `optional` — subagents is never a blocker for the spine. + - `pi-ui-extension-patterns` (FE-744) tied off 2026-06-01; see docs/archive/PLAN_HISTORY.md. ``` diff --git a/memory/SPEC.md b/memory/SPEC.md index c5eb1360e..1dc43b9da 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -104,18 +104,18 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | A4-L | A monotonic global LSN per commit (one-LSN-per-transaction) is adequate for change-log replay, reconciliation-need ordering, and mention staleness without per-row vector clocks. | high | open | I1-L, I4-L | M4 + M7: replay fidelity and `worldUpdate` ordering tests. | | A5-L | Agent-as-user probes over the public Brunch RPC surface can produce regression-quality transcript artifacts without depending on a parallel brief-library subsystem. | medium | partially validated | D5-L, D48-L, D49-L | FE-744 public-RPC parity proves the deterministic transport/projection substrate for current structured-exchange permutations; future brief-based or generative golden-fixture work must enter through the probe/transcript artifact path. | | A6-L | The graph-native vocabulary can be deferred from explicit per-plane namespacing (`intent.*`, `oracle.*`, etc.) and start unified under `graph.*` without painful rework later. | medium | open | D3-L | M4–M5: if intent-plane plus oracle-plane stubs both fit under one namespace cleanly, the assumption holds. | -| A7-L | `framing_as` as an orthogonal modality on existing node kinds is sufficient for product-intent ontology (problem, persona, JTBD, etc.) and does not need to become first-class node kinds in the POC. | medium | open | D7-L | Targeted probe runs that exercise framing pressure: if a framing repeatedly demands unique relation policy, promote per the seam-extensions Open Question #8. | +| A7-L | ~~`framing_as` as an orthogonal modality on existing node kinds is sufficient for product-intent ontology.~~ | — | **retired** | D7-L, D54-L, D56-L | Validated and retired by Phase 2 node lock: `framing_as` is absorbed by first-class `thesis`, `term`, and `constraint.subtype` kinds plus `goal`. The modality, allowed matrix (I7-L), and "promote on relation-policy pressure" escape hatch are all retired. See [`docs/design/GRAPH_MODEL.md` §framing_as — retired](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#framing_as--retired). | | A8-L | One reconciliation-need substrate, sharing the same global LSN as the change log, can absorb impasses, conflicts, gaps, and process debt without needing finer kind subtypes in the POC. | medium | open | D8-L | M8 + adversarial fixtures ("contradictory requirements") exercise the substrate; subtype split deferred per Open Question #10. | | A9-L | A session-scoped mention ledger of (`entity_id`, `snapshotted_lsn`) is the right granularity for staleness hints; transcript-scoped or graph-scoped ledgers are not needed for the POC. | low | open | I7-L | M7 — turn-boundary reconciliation slice; observed via fixture runs that stress re-read decisions. | | A11-L | Pi's `prepareNextTurn` plus custom-message delivery are sufficient to express side-task result delivery without inventing a second event plane or forking pi. | medium | open | D15-L | M5 + M7: side-task registry wiring and next-turn delivery proof. | | A13-L | If Brunch later adds deferred observer/auditor jobs, a durable queue keyed by session id and elicitation-exchange entry range can recover async audit/backfill after process interruption without reintroducing canonical chat/turn tables; whether this shares storage with a generalized work-item/reconciliation table can be deferred. | medium | open | D18-L, I14-L | Deferred until async audit/backfill lands: restart/idempotence tests exercise exchange-keyed jobs once graph writes exist. | -| A14-L | LLM elicitor agents can reliably produce graph-structurally-legal review-set proposals (well-formed entity drafts and semantic edges that pass `CommandExecutor` structural validation). | medium | open | D27-L | Probe runs that exercise batch-proposal and commitment review-set flows; dry-run `CommandExecutor` validation at proposal time before user review. Fallback (constrained generation, retry-with-feedback, or NL-parse-at-accept) preserves the user-facing review-cycle if reliability is insufficient. | +| A14-L | LLM elicitor agents can reliably produce graph-structurally-legal graph mutations — both `commitGraph` batches (D53-L) and review-set proposals (D27-L) — as well-formed entity drafts and category-typed edge drafts per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) that pass `CommandExecutor` structural validation. The `commitGraph` path under `propose-graph` strategy (D26-L) is the primary proof target: the agent must produce valid multi-node multi-edge batches with intra-batch references from a graph-vocabulary prompt after concept-level user acceptance. | medium | open | D27-L, D51-L, D53-L | Build graph layer (db/ + graph/ CommandExecutor) and commitGraph tool, then test LLM generation against real CommandExecutor validation. Bounded retry on structural_illegal. Fallback (constrained generation, retry-with-feedback, or NL-parse-at-accept) preserves the user-facing flow if reliability is insufficient. | | A15-L | Establishment hints as transcript-native custom entries (`brunch.establishment_offer`) provide sufficient inspectability, fixture-ability, and ambient-affordance source without a separate establishment-needs graph substrate; whether such a substrate ever shares storage with reconciliation needs can be deferred. | medium | open | D25-L, D30-L | M5+: fixture inspection confirms lens offers are reconstructable from transcript; chrome region renders ambient affordances from the latest such entry. | | A16-L | Reviewer triggering policy (always-on vs lens-keyed) and reviewer scope (batch + how-far-neighborhood) can be deferred to per-lens decisions without architectural commitment now. | low | open | D29-L | M5+: empirical — reviewer integration reveals which policy avoids unacceptable next-turn latency without losing relevant findings. | | A17-L | A user-level temperamental preference for interrogative vs proposal-based elicitation meaningfully affects adoption and eventually warrants expression as a user-level setting. | low | open | D25-L, D26-L | Deferred; surfaces from outer-loop walkthroughs and adversarial fixtures once both single-exchange and batch-proposal flows exist in product. | | A18-L | Hiding unsupported Pi built-ins from autocomplete plus blocking dangerous session effects is sufficient for the POC product shell even though exact interactive built-ins remain callable until Pi exposes command policy. | medium | open | D2-L, D24-L, D34-L, D35-L | `pi-ui-extension-patterns` product-shell review after command-containment and dynamic Brunch chrome evidence; strict suppression requires a Pi upstream/API change if residual exposure is unacceptable. | | A19-L | Pi's current settings/resource lifecycle can be made product-safe through a sealed Brunch Pi Profile without forking Pi: ambient discovery remains disabled, Brunch-owned extension factories may inject explicit resources, and remaining settings/keybinding leakage can be eliminated through programmatic policy or a narrow upstream seam. | medium | open | D39-L | FE-744/profile audit: source-backed resource-loader/settings audit, tests proving no ambient `.pi/` skills/prompts/themes/extensions/context files affect Brunch, and product-owned resources still load when intentionally injected. | -| A20-L | The Drizzle 1.0 beta line (specifically `drizzle-orm@^1.0.0-beta.15` or later, with the built-in `drizzle-orm/typebox` path that consumes the new `typebox` package) is stable enough for Brunch to depend on for M4 graph persistence and beyond. | medium | open | D16-L, D41-L | M4 scoping spike: round-trip `drizzle-orm@1.0.0-beta.*` + `drizzle-orm/typebox` + `better-sqlite3` + Pi `registerTool` over a representative intent-plane table; if beta blocks land (migrations, SQLite type fidelity, or schema-derivation bugs), fall back to Drizzle 0.x + standalone `drizzle-typebox` + `drizzle-orm/typebox-legacy` and re-evaluate per release. | +| A20-L | The chosen Drizzle line and row-schema derivation path can be settled during the prep envelope without forcing later M4 rework: Brunch can prove migrations, SQLite fidelity, monotonic counter allocation, change-log writes, and runtime-schema derivation on one representative persistence slice before CRUD proper starts. | high | **validated** | D16-L, D41-L | **Validated by A20-L spike (2026-06-01).** Stack: `drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` + `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14`. Proved: (1) `drizzle-typebox` derives valid TypeBox insert/select schemas from Drizzle tables; `Value.Check` validates/rejects correctly. (2) Batch `commitGraph`-shaped transaction (multi-node → intra-batch ref resolution → multi-edge → LSN allocation → change-log append) works atomically; full rollback on FK violation or domain-validation throw. (3) `update().returning()` works for atomic monotonic counter increment; `insert().returning()` gives auto-increment IDs for ref resolution; JSON detail column round-trips cleanly. (4) Pi tool parameters (`typebox` v1.x) and Drizzle row schemas (`@sinclair/typebox` v0.34 via `drizzle-typebox`) serve different roles and never cross — shared enum `const` arrays bridge both. | | A21-L | The POC can treat coherence as a bounded product verdict over structural legality plus explicitly detected contradictions, gaps, and unresolved reconciliation needs, without solving a general theory of “spec coherence.” | low | open | D8-L | M8 must sharpen the coherence rubric before implementation: known-bad adversarial briefs should show what counts as incoherent, what is merely immature/underspecified, and what should become a reconciliation need. | | A22-L | The elicitor can perform synchronous post-exchange capture well enough for the POC: high-confidence extractive facts and readiness/posture updates can be committed immediately, while low-confidence implications can be kept out of graph truth and used as disambiguation material. | medium | open | D18-L, D26-L, D45-L, I30-L | M5 agent-graph-integration fixtures and review: compare elicitor-captured graph updates against transcript evidence; track over-capture, missed obvious facts, and whether preface-led disambiguation resolves low-confidence material without an async observer owning primary extraction. | @@ -125,23 +125,30 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D1-L — Depend on `pi-coding-agent`, not only `pi-agent-core`.** The POC reuses the coding-agent service bundle, TUI/print adapters, RPC machinery, session logging, and tool plumbing. Dropping down to `pi-agent-core` is a fallback if Brunch proves too different. Depends on: A1-L. Supersedes: —. - **D2-L — Brunch is an opinionated product, not a pi platform shell.** The POC hardcodes its toolset, system prompt, and policy doctrine; scopes state to `.brunch/`; and hides pi's generic extension surface from end users. Depends on: A1-L. Supersedes: —. -- **D39-L — Brunch owns a sealed Pi Profile around the embedded harness.** Product behavior must come from Brunch-owned programmatic policy, not ambient Pi discovery. The profile includes settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. Current known posture disables ambient context files, extensions, prompt templates, skills, and themes while loading Brunch's inline extension shell; Pi source confirms extension `resources_discover` can still inject explicit Brunch-owned skill/prompt/theme paths even when `noSkills`/`noPromptTemplates`/`noThemes` disable ambient discovery. Brunch-owned Pi extensions are loaded by an explicit product shell (`src/tui-client/pi-extension-shell.ts`) rather than ambient discovery; *explicit* means the shell statically imports its product extensions and registers them from a fixed ordered list — it must not filesystem-discover or dynamically `import()` extension modules at runtime, because a Brunch-internal discovery layer is itself the discovery this decision rejects. Each product extension exposes one registrar taking explicit dependencies, and the shell wires those dependencies at the call site; the `default` exports under `src/tui-client/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/tui-client/.pi/extensions/*`, and reusable Pi TUI components live under `src/tui-client/.pi/components/*`, so they can also be iterated by launching Pi from `src/tui-client` and using `/reload`; the root project-local `.pi/` probe runtime files are retired and must not be treated as product configuration. Test files must not live directly under auto-discovered `.pi/extensions` or `.pi/components` resource directories; TUI-client extension/component tests live under `src/tui-client/.pi/__tests__/`. The remaining weak point is settings leakage through `SettingsManager.create(cwd, agentDir)`, currently only overriding quiet startup; Brunch must audit and either override/seal settings that affect product behavior (shell path/prefix, compaction/retry, image handling, keybindings if exposed) or request a narrow Pi seam. Depends on: D1-L, D2-L, A19-L. Supersedes: treating `noSkills: true` as full profile isolation, relying on user/project `.pi/` defaults to be harmless, nesting Brunch's product extension modules under `src/tui-client/.pi/extensions/brunch/`, or replacing the explicit static shell list with a Brunch-internal filesystem-discovery / `brunchExtensionMeta` / `loadOrder` mechanism as the product runtime load path. +- **D39-L — Brunch owns a sealed Pi Profile around the embedded harness.** Product behavior must come from Brunch-owned programmatic policy, not ambient Pi discovery. The profile includes settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. Current known posture routes TUI launch policy through `src/brunch-pi-profile.ts`, creates an in-memory Brunch-owned `SettingsManager` policy instead of reading ambient global/project `.pi/settings.json`, disables ambient context files, extensions, prompt templates, skills, and themes while loading Brunch's inline extension shell, and defaults Brunch-launched Pi to offline mode; Pi source confirms extension `resources_discover` can still inject explicit Brunch-owned skill/prompt/theme paths even when `noSkills`/`noPromptTemplates`/`noThemes` disable ambient discovery. Brunch-owned Pi extensions are loaded by an explicit product shell (`src/tui-client/pi-extension-shell.ts`) rather than ambient discovery; *explicit* means the shell statically imports its product extensions and registers them from a fixed ordered list — it must not filesystem-discover or dynamically `import()` extension modules at runtime, because a Brunch-internal discovery layer is itself the discovery this decision rejects. Each product extension exposes one registrar taking explicit dependencies, and the shell wires those dependencies at the call site; the `default` exports under `src/tui-client/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/tui-client/.pi/extensions/*`, and reusable Pi TUI components live under `src/tui-client/.pi/components/*`, so they can also be iterated by launching Pi from `src/tui-client` and using `/reload`; the root project-local `.pi/` probe runtime files are retired and must not be treated as product configuration. Test files must not live directly under auto-discovered `.pi/extensions` or `.pi/components` resource directories; TUI-client extension/component tests live under `src/tui-client/.pi/__tests__/`. The profile boundary now owns the audited behavior-shaping settings list in code (`BRUNCH_SETTINGS_POLICY` / `BRUNCH_SETTINGS_AUDITED_GETTERS`), with hostile ambient settings and reload-resilience tests covering shell path/prefix, npm command, ambient resources, skill commands, double-escape behavior, compaction/retry, image/terminal/UI, transport/theme/changelog, and telemetry settings. Remaining profile work is runtime-state/prompt/tool posture, not ambient settings file leakage. Depends on: D1-L, D2-L, A19-L. Supersedes: treating `noSkills: true` as full profile isolation, relying on user/project `.pi/` defaults to be harmless, nesting Brunch's product extension modules under `src/tui-client/.pi/extensions/brunch/`, or replacing the explicit static shell list with a Brunch-internal filesystem-discovery / `brunchExtensionMeta` / `loadOrder` mechanism as the product runtime load path. - **D40-L — Runtime posture is a transcript-backed Brunch state machine, not hidden extension memory.** Brunch distinguishes operational modes (`elicit`, future `execute`) from agent roles (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor roles) and from strategies/lenses. The active top-level role is selected through a role preset/runtime bundle that derives model, thinking level, prompt packs, allowed strategies/lenses, and tool policy rather than storing each knob independently. Brunch runtime helpers append full selected-state product custom entries under `brunch.agent_runtime_state` with `reason: "init" | "switch"`; turn preparation projects the latest valid linear transcript snapshot into prompt and tool posture. Brunch product prompt packs are private code-composed assets under `src/tui-client/.pi/context/prompt-packs/*`, composed only through `src/tui-client/.pi/context/compose-brunch-prompt.ts` and appended by the explicit `src/tui-client/.pi/extensions/prompting.ts` product extension; they are not Pi prompt templates, skills, context-file discovery, or user-invoked slash-command resources. The current `elicit` tool policy is a denylist over side-effecting tools (`bash`, `edit`, `write`) plus user-shell interception, so new safe Brunch extension tools are not hidden by a stale allowlist. The Pi extension module that owns tool policy is `src/tui-client/.pi/extensions/operational-mode.ts`, while product prompting is owned separately by `src/tui-client/.pi/extensions/prompting.ts`; neither should duplicate the other's control-plane responsibility. Depends on: D17-L, D23-L, D25-L, D39-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority for agent behavior, modeling read-only posture as a volatile allowlist of every safe tool, or exposing Brunch prompt packs through Pi resource discovery. - **D34-L — Command containment separates visibility suppression from effect blocking.** Current Pi extension seams can hide unsupported slash suggestions with autocomplete wrapping and can cancel branch/session effects through lifecycle hooks, but they cannot strictly suppress exact interactive built-in commands before `InteractiveMode` dispatches them. Brunch-owned commands must use product-specific names and route writes through Brunch handlers/`CommandExecutor`; extension command collisions are not an override mechanism. Strict built-in command/keybinding policy is a Pi upstream/API ask, while POC safety relies on hiding generic affordances, blocking dangerous effects (`/fork`, `/clone`, `/tree`, raw session replacement), and failing fast on branched transcripts. Brunch's command-policy code should live in `src/tui-client/.pi/extensions/command-policy.ts`, merging branch/session-effect blocking with any product command allow/deny behavior instead of preserving a branch-only module. Depends on: D2-L, D24-L, A18-L. Supersedes: treating extension `input` handlers or command-name collisions as built-in command allowlisting. - **D35-L — Dynamic TUI chrome is a Brunch projection wrapper over Pi UI primitives.** Downstream TUI affordances should call a Brunch-owned renderer (`renderBrunchChrome` or its successor) with one activated product-state snapshot rather than scattering raw `ctx.ui.setHeader`, `setFooter`, `setWidget`, title, or working-indicator calls. The wrapper is stateless projection over canonical workspace/session/graph facts, including the real activated session id, while its TUI footer compositor may read Pi footer telemetry (`getGitBranch`, foreign `getExtensionStatuses`) at render time. Brunch chrome does not publish a `brunch.chrome` status key; `ctx.ui.setStatus(key, text)` remains a lateral contribution channel for other extensions and future dynamic Brunch state. RPC clients should rely only on surfaces Pi actually emits for the wrapper (currently diagnostic widget/title, plus any future explicit status adapter) because header/footer/working-indicator are TUI-only in current Pi RPC mode. Session display names are likewise product projections over Pi session metadata: Brunch may append Pi `session_info` entries, but generated names must characterize the selected spec/session transcript rather than replace spec identity or graph truth. Depends on: D2-L, D21-L, D34-L, A18-L. Supersedes: treating Pi UI methods as direct downstream affordance APIs, rendering placeholder session state such as `unbound` after a session is activated, or consuming the status-key namespace for chrome's own static summary. +- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by axis (`modes/`, `strategies/`, `lenses/`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, and the state definitions that drive mode/role/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/tui-client/.pi/context/`. #### Data model & vocabulary - **D3-L — Graph-native, session-native vocabulary; no generic `records.*` surface.** Commands converge on `graph.*` / `session.*` (with per-plane families `intent.*`, `oracle.*`, `design.*`, `plan.*` available when sharper semantics are useful). Depends on: A6-L. Supersedes: —. -- **D7-L — `framing_as` modality, not first-class kinds, for product-intent framings.** Product framings (problem, persona, JTBD, non-goal, etc.) are an orthogonal modality on existing intent/constraint node kinds, gated by an allowed matrix. Depends on: A7-L. Supersedes: —. -- **D8-L — Reconciliation needs are a first-class substrate alongside graph truth, change log, and a bounded coherence verdict.** Needs (impasses, gaps, contradictions, process debt) share the same global LSN as the change log and follow the same mutation invariant. For the POC, coherence is not an unbounded aesthetic or philosophical judgment; it is the product-visible verdict produced from structural legality plus surfaced contradictions/gaps/unresolved needs, with the exact rubric still open under A21-L until M8. Depends on: A8-L, A21-L. Supersedes: —. -- **D9-L — Reasoning records split by shape.** `decision` is graph-native; `impasse` is a reconciliation need, not a graph node; `justification` stays compact (rendered text on the decision) until forced otherwise. Depends on: D8-L. Supersedes: —. +- **D7-L — ~~`framing_as` modality, not first-class kinds.~~ Retired.** `framing_as` is absorbed by first-class `thesis`, `term`, `constraint.subtype`, and `goal` kinds per the Phase 2 node lock. No node carries a `framing_as` field. See [`docs/design/GRAPH_MODEL.md` §framing_as — retired](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#framing_as--retired). Depends on: A7-L (retired). Superseded by: D54-L, D56-L. +- **D8-L — Reconciliation needs are a first-class substrate alongside graph truth, change log, and a bounded coherence verdict.** Needs (impasses, gaps, contradictions, process debt) share the same global LSN as the change log and follow the same mutation invariant. Per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#reconciliationneed--separate-substrate-not-a-graph-edge), each need targets exactly one of `{kind: 'edge', edgeId}` or `{kind: 'node_pair', aId, bId}` and is not itself a graph edge. For the POC, coherence is not an unbounded aesthetic or philosophical judgment; it is the product-visible verdict produced from structural legality plus surfaced contradictions/gaps/unresolved needs, with the exact rubric still open under A21-L until M8. Depends on: A8-L, A21-L. Refined by: D51-L. Supersedes: any `concerns`-edge wiring from reconciliation needs to graph nodes. +- **D9-L — Reasoning records split by shape.** `decision` is graph-native; `impasse` is a reconciliation need, not a graph node; `justification` stays compact (rendered text on the decision) until forced otherwise. Phase 2 (per `docs/design/GRAPH_MODEL.md`) keeps `decision` as a plain node rather than a hyper-edge / hub-node for the POC. Depends on: D8-L. Supersedes: —. +- **D54-L — Graph node shape is a common flat interface with `title`, `body`, `basis`, `source`, and a per-kind `detail` JSON column; canonical contract is [`docs/design/GRAPH_MODEL.md` §GraphNode](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#graphnode--the-single-shape).** All planes and kinds share one `nodes` table. `plane` determines which closed `kind` enum applies; `kind` is structurally validated. `basis ∈ explicit | accepted_review_set` (same semantics as edges). `source` is a free-form string for epistemic attribution (e.g. "stakeholder", "regulatory", "derived") — convention by prompt, not structural validation; it exists for context-snapshot enrichment and will be rendered back into sparse text, not used for policy or filtering. `detail` is an optional JSON column with per-kind validated sub-structures: `decision` requires `{ chosen_option, rejected, rationale }`, `term` requires `{ definition, aliases? }`; all other kinds must omit `detail`. `provenance` is retired from the node shape — `change_log` at `createdAtLsn` owns all audit trail. The intent kind rubric (modality of claim + source question per kind) is agent-facing prompting guidance in GRAPH_MODEL.md §"Prompting guidance for kind discrimination", not structural enforcement. Depends on: D4-L, D16-L, D52-L, D56-L. Supersedes: D7-L (`framing_as` modality), the deferred Phase 2 node placeholder in prior GRAPH_MODEL.md. +- **D55-L — `provenance` retired from both edges and nodes; `change_log` owns all audit trail.** Transcript entry pointers (`sessionId`, `entryId`, `proposalEntryId`) are fragile under compaction and redundant with `change_log` + `basis`. `basis` tells you the authority path; `change_log[createdAtLsn]` tells you the durable audit context. Edges retain `basis` and `rationale`. Nodes have `basis` and `source` (epistemic attribution). Depends on: D16-L, D51-L, D54-L. Supersedes: `EdgeProvenance` from Phase 1 edge lock, the planned node-side `provenance` symmetry with edges. +- **D56-L — Intent node kinds: 11 kinds in 3 derived categories (basic / structural / reasoning); canonical contract is [`docs/design/GRAPH_MODEL.md` §Per-plane node kinds](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#per-plane-node-kinds).** `basic` (goal, thesis, term, context) carries grounding material; `structural` (requirement, assumption, constraint, invariant) carries core specification; `reasoning` (decision, criterion, example) carries decisions and evidence. Category is a pure function of `kind` — not stored on the node. The `basic` category maps to spec-grade progression: grounding-gate readiness depends on satisficing threshold of basic-category nodes (D57-L). `thesis` carries "what/who/why/for whom" material (La Carte Blanche style). `term` carries canonical naming commitments (ubiquitous language). `invariant` is first-class (not a constraint subtype) because its operational role differs: invariants get `dependency` and `proof` edges, constraints get `boundary` edges. Each intent kind has a modality-of-claim and source-question rubric for agent prompting (GRAPH_MODEL.md §"Prompting guidance"). Oracle (check, validation_method, evidence, obligation), design (module, interface), and plan (milestone, frontier, slice) kinds are stable from worked examples. Depends on: D54-L. Supersedes: D7-L (`framing_as`), A7-L. +- **D57-L — Spec-grade grounding gate is LLM-judged satisficiency with a count floor on basic-category nodes.** The gate from `grounding_onboarding` toward `elicitation_ready` is not structurally enforced by rubric coverage checks. The agent judges readiness using prompt-embedded abstract drivers (Walter-style: what is it, who is it for, what problem, what value, when used, how measured) but cannot declare grounding complete with zero `basic`-category nodes. Grounding elicitation interweaves basic intent nodes with spec-level posture establishment. `posture` is a spec-level property set, not a graph node kind. Depends on: D45-L, D56-L. Supersedes: D30-L grounding-bundle anchor vocabulary as the sole readiness gate description. Refines: D30-L, D45-L. +- **D51-L — Graph edge model is a closed structural-category set with a separate ReconciliationNeed substrate; canonical contract is [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md).** Every accepted edge is one of eight closed categories (`dependency`, `proof`, `support`, `realization`, `boundary`, `composition`, `association`, `supersession`); `stance: for | against` is valid only on `proof` and `support`; `basis ∈ explicit | accepted_review_set` (no `inferred`). Accepted edges have no mutable `status` field — `proposed` lives in review-set drafts, `rejected` is absent + change-log audit, `stale` is represented by a `ReconciliationNeed`. Identity fields (`category`, `sourceId`, `targetId`, `stance`) are immutable on an accepted edge; a "category change" is delete + recreate. Only `dependency` cascades automatically; other categories surface advisory recon-needs rather than auto-blocking. Cross-plane edges are unrestricted at the POC stage; `realization` subtypes (implementation/establishment/assertion/etc.) may be derived from node-tuple lookup later rather than encoded on the edge. `ReconciliationNeed` is a separate substrate whose target is exactly `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` — it is not itself a graph edge. Depends on: D4-L, D8-L, D16-L, D27-L, A14-L. Supersedes: the named-relation catalogue in `docs/architecture/pi-seam-extensions.md` §"Edge types" (`validates`, `instance_of`, `produces`, `discharges`, `depends_on`, `derived_from`, `counterexample_for`, `witnesses`), the per-relation policy registry / lookup, the brainstormed expanded edge taxonomy in `archive/docs/design/GRAPH_EDGE_CATEGORIES.md`, and any `concerns`-edge wiring from reconciliation needs to graph nodes. #### Authority & mutation - **D4-L — One shared mutation surface owns graph truth.** Every semantic graph mutation routes through Brunch-owned typed command handlers responsible for validation, structural legality, optimistic concurrency, event emission, audit attribution, and coherence triggering. Agents and adapters must not touch the ORM or SQLite directly. Depends on: A3-L. Supersedes: —. - **D20-L — Command execution owns the pre-M6 authority seam.** Callers submit product commands to a Brunch `CommandExecutor` and receive a structured result; they do not call a standalone authority service or graph persistence directly. The executor is the public mutation boundary that hides attribution, optimistic concurrency, structural validation, the minimal pre-M6 policy classifier, transaction execution, LSN allocation, change-log append, and coherence-trigger hooks. Before M6, the policy logic may be deliberately small, but the result shape must already include `needs_human`, `policy_blocked`, `version_conflict`, and `structural_illegal` so early RPC, print, agent-tool, deferred observer/auditor, and side-task code cannot bake in permissive mode-specific shortcuts. Depends on: D4-L, D16-L. Supersedes: the separate optional `AuthorityGate` / generic policy-service mental model. -- **D27-L — Review-set proposals are structured entity-draft payloads; batch acceptance is one atomic `CommandExecutor` call.** The elicitor's proposal custom entry (`brunch.review_set_proposal`) contains the graph entities and edges that *would* be created on acceptance, in a form `CommandExecutor` can dry-run-validate at proposal time so `structural_illegal` / `policy_blocked` discriminants surface before the user reviews. Only proposals that pass this dry-run validation are surfaced as user-reviewable review sets; invalid generations stay internal to retry/regeneration paths rather than becoming review UI state. Acceptance is one `acceptReviewSet` command that consumes one LSN, writes the entire batch in one transaction, appends one change-log entry attributed to the user, triggers coherence updates, and enqueues any reviewer job. "Accept with edits" does not exist as a primitive: the cycle is approve / request changes (triggers regeneration of a successor proposal) / reject. Applies to batch-proposal flows and commitment review sets. Depends on: A14-L, D4-L, D20-L, D26-L. Supersedes: any caller-side multi-step "patch then commit" mental model. +- **D27-L — Review-set proposals are structured entity-draft payloads; batch acceptance is one atomic `CommandExecutor` call.** The elicitor's proposal custom entry (`brunch.review_set_proposal`) contains the graph entities and edges that *would* be created on acceptance — edge drafts follow the locked contract in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (`{category, sourceId, targetId, stance?, basis: 'accepted_review_set'}`) rather than a free-form `relation` string — in a form `CommandExecutor` can dry-run-validate at proposal time so `structural_illegal` / `policy_blocked` discriminants surface before the user reviews. Only proposals that pass this dry-run validation are surfaced as user-reviewable review sets; invalid generations stay internal to retry/regeneration paths rather than becoming review UI state. Acceptance is one `acceptReviewSet` command that consumes one LSN, writes the entire batch in one transaction, appends one change-log entry attributed to the user, triggers coherence updates, and enqueues any reviewer job. "Accept with edits" does not exist as a primitive: the cycle is approve / request changes (triggers regeneration of a successor proposal) / reject. Applies to batch-proposal flows and commitment review sets. Depends on: A14-L, D4-L, D20-L, D26-L. Supersedes: any caller-side multi-step "patch then commit" mental model. +- **D53-L — `commitGraph` is a single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references.** The propose-graph strategy's load-bearing tool for direct graph commitment after concept-level user acceptance (D26-L). The agent produces one tool call with a `nodes` array (each carrying a temporary batch `ref`, `kind`, and content fields) and an `edges` array (each carrying `category`, source/target as either an intra-batch `ref` or an existing node id, optional `stance`, and `rationale`). The CommandExecutor validates all nodes structurally, assigns real NodeIds to each batch ref, resolves intra-batch and existing-node references, validates all edges per the closed category set and structural invariants in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), allocates one LSN, writes all nodes + edges + change-log in one SQLite transaction, and returns success with created ids or `structural_illegal` with diagnostics sufficient for agent self-correction. On validation failure the agent may retry within a bounded budget; the user does not see intermediate failures. `commitGraph` and `acceptReviewSet` (D27-L) are parallel paths to the same CommandExecutor — one for direct agent-authored commits after concept acceptance, one for user-reviewed batch proposals. Depends on: D4-L, D20-L, D51-L, D52-L. Supersedes: —. #### Transport & client @@ -198,7 +205,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D6-L — JSONL-first transcript persistence in `.brunch/sessions/`; SQLite-backed graph persistence in `.brunch/`.** Two durability surfaces with distinct responsibilities. Transcript starts on pi `SessionManager` redirected to the project-local directory; graph plane is SQLite from M4. Brunch does not recreate canonical `chat` or `turn` tables while Pi JSONL remains viable for Brunch-supported linear sessions. Validated by M2. Supersedes: —. - **D15-L — Side tasks are a first-class Brunch subsystem delivered through the same transcript/event substrate.** Side tasks are main-agent-invoked, non-blocking work items: the main agent fires them and continues without awaiting a return value. A Brunch-owned `SideTaskRegistry` tracks status; the only path a side task influences the main agent is by appending a custom-message status update to the session log that arrives at the next-turn boundary through the existing `prepareNextTurn` path — never mid-turn. Side-task writes remain subject to the same command-layer authority as primary-agent writes. This is distinct from D44-L Subagent (main-agent-invoked **blocking** tool call whose result is returned directly as tool content). Depends on: A11-L, D4-L. Supersedes: —. -- **D16-L — Graph persistence uses Drizzle over `better-sqlite3`, with one-LSN-per-commit and no bypass paths.** The command layer owns precondition checks, structural validation, entity writes, LSN allocation, change-log append, and any coherence updates inside one transaction. This rule applies equally to migrations and maintenance code; there is no privileged write path outside the command-executor protocol. Runtime row/insert/update schemas are derived from Drizzle table definitions through a D41-L-compatible adapter (`drizzle-zod`, `drizzle-orm/typebox`, or equivalent chosen during the A20-L graph-data-plane spike) rather than hand-authored alongside the table. The Drizzle version pin is open per A20-L. Depends on: A3-L, A4-L. Refined by: D41-L. Supersedes: —. +- **D16-L — Graph persistence uses Drizzle over `better-sqlite3`, with one-LSN-per-commit and no bypass paths.** The command layer owns precondition checks, structural validation, entity writes, LSN allocation, change-log append, and any coherence updates inside one transaction. This rule applies equally to migrations and maintenance code; there is no privileged write path outside the command-executor protocol. Runtime row/insert/update schemas are derived from Drizzle table definitions through `drizzle-typebox` (`createInsertSchema`, `createSelectSchema`) rather than hand-authored alongside the table. **Settled by A20-L spike (2026-06-01):** `drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` + `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14`. Pi tool parameter schemas use `typebox` v1.x (Pi's package) separately; Drizzle-derived row schemas stay internal to `db/`→`graph/`; shared enum `const` arrays bridge both. Depends on: A3-L, A4-L, A20-L (validated). Refined by: D41-L. Supersedes: —. - **D18-L — Post-exchange capture is synchronous elicitor work for the POC; observer/auditor queues are deferred backstops, not primary extraction authority.** After a user response closes an elicitation exchange, the elicitor may run a post-exchange capture step in the same turn-boundary flow: commit high-confidence extractive facts, concrete reconciliation needs, and spec readiness/posture updates through the `CommandExecutor`; fold low-confidence implications into later questions rather than graph truth. Brunch may still introduce durable observer/auditor jobs keyed by session id plus exchange entry ids for restartable audit, quality checks, or later backfill, but those jobs are not the load-bearing path for keeping the next turn's world fresh. Any async job writes still route through the command layer and remain operational queue state unless they surface semantic work as reconciliation needs. Depends on: A13-L, A22-L, D4-L, D13-L, D16-L. Supersedes: the old DB-backed `chat` / `turn` mental model and the earlier observer-owned primary extraction path. - **D28-L — Regenerated review-set proposals are appended as successor entries in the linear Pi JSONL session; projection helpers filter to the accepted set for context economy.** When the user requests changes, the agent appends a successor proposal entry that references its predecessor via `supersedes`; prior proposals are *not* deleted from JSONL but remain visible as raw transcript history. This stays within Brunch's linear transcript policy — no Pi branching is created. Pi JSONL is treated as a "capture everything" store for replay and audit. Projection helpers used to drive the agent (context injection, summarization) walk the `supersedes` chain and surface only the latest (or ultimately accepted) proposal — the agent does not re-process every superseded proposal as live context. The reviewer likewise sees only the accepted set, not the regeneration history. Depends on: D6-L, D12-L, D17-L, D24-L, D27-L. Supersedes: any "in-place edit" or "fork-on-regenerate" mental model. - **D29-L — Reviewer is an async advisory role with narrow write authority.** After a batch acceptance closes, Brunch may enqueue a reviewer job keyed by session id plus the batch-acceptance entry id; the job survives process restart and analyzes the accepted batch plus its graph neighborhood for coherence, completeness, and gaps. **Reviewer writes only `reconciliation_need` records via the `CommandExecutor`**; it never writes graph entities, edges, change-log entries directly, or any other record class. Findings reach the user through next-turn delivery as advisory items on the reconciliation-need surface — the batch acceptance remains the user's atomic commitment and the reviewer cannot amend it. (Suggestion-shaped findings may later route to candidate-artefacts when that substrate exists; the POC routes everything to reconciliation needs.) Depends on: A16-L, D4-L, D8-L, D15-L, D17-L, D18-L, D20-L, D27-L. Supersedes: any "reviewer may quietly amend the graph" mental model. @@ -207,7 +214,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c #### Schema & validation -- **D41-L — Boundary schemas are runtime-validated and JSON-Schema-exportable; Zod v4 may be the product/protocol schema source.** Brunch boundary shapes must have one runtime schema source of truth, derived static TypeScript types, and JSON Schema output wherever a public protocol or Pi tool boundary needs discoverability. Zod v4 is permitted — and preferred for structured-exchange product/protocol schemas — when the schema stays inside Zod's JSON-representable subset and tests prove `z.toJSONSchema(..., { unrepresentable: "throw" })` succeeds for the exported boundary. TypeBox remains valid for Pi tool parameter objects, small config/frontmatter contracts, and any seam where the direct JSON-Schema-shaped authoring style is cheaper. Do not hand-author parallel Zod and TypeBox definitions for the same boundary; if a Pi API requires a JSON-Schema-shaped object, generate or adapt it from the chosen source schema and test the adapter. Drizzle table definitions remain canonical for persisted shapes; row/insert/update validation must be derived from Drizzle through a single adapter path (`drizzle-zod`, `drizzle-orm/typebox`, or equivalent selected during A20-L) rather than hand-authored alongside the table. Boundary Zod schemas must avoid transforms, `Date`, `Map`, `Set`, `bigint`, and other unrepresentable constructs unless an explicit adapter owns the input/output split and JSON Schema export tests cover it; refinements are allowed only for runtime constraints that stay inside JSON-representable input/output shapes and are covered by parse tests plus export tests. Static TS types come from the schema source (`z.infer` for Zod, `Static` for TypeBox); runtime parsing uses the matching library (`zExample.parse`/`safeParse` for Zod, `Value.Parse`/`Value.Check` for TypeBox). Depends on: D4-L, D5-L, D16-L. Supersedes: TypeBox as Brunch's single runtime schema vocabulary, the ban on Zod outside downstream adapters, and an implicit "any runtime schema library is fine" posture. +- **D41-L — Boundary schemas are runtime-validated and JSON-Schema-exportable; Zod v4 may be the product/protocol schema source.** Brunch boundary shapes must have one runtime schema source of truth, derived static TypeScript types, and JSON Schema output wherever a public protocol or Pi tool boundary needs discoverability. Zod v4 is permitted — and preferred for structured-exchange product/protocol schemas — when the schema stays inside Zod's JSON-representable subset and tests prove `z.toJSONSchema(..., { unrepresentable: "throw" })` succeeds for the exported boundary. TypeBox remains valid for Pi tool parameter objects, small config/frontmatter contracts, and any seam where the direct JSON-Schema-shaped authoring style is cheaper. Do not hand-author parallel Zod and TypeBox definitions for the same boundary; if a Pi API requires a JSON-Schema-shaped object, generate or adapt it from the chosen source schema and test the adapter. Drizzle table definitions remain canonical for persisted shapes; row/insert/update validation must be derived from Drizzle through a single adapter path (`drizzle-zod`, `drizzle-orm/typebox`, or equivalent selected during the A20-L prep-envelope spike) rather than hand-authored alongside the table. Boundary Zod schemas must avoid transforms, `Date`, `Map`, `Set`, `bigint`, and other unrepresentable constructs unless an explicit adapter owns the input/output split and JSON Schema export tests cover it; refinements are allowed only for runtime constraints that stay inside JSON-representable input/output shapes and are covered by parse tests plus export tests. Static TS types come from the schema source (`z.infer` for Zod, `Static` for TypeBox); runtime parsing uses the matching library (`zExample.parse`/`safeParse` for Zod, `Value.Parse`/`Value.Check` for TypeBox). Depends on: D4-L, D5-L, D16-L. Supersedes: TypeBox as Brunch's single runtime schema vocabulary, the ban on Zod outside downstream adapters, and an implicit "any runtime schema library is fine" posture. #### Interaction & UI shape @@ -219,8 +226,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D38-L — JSON-over-editor is the Pi-RPC compatibility seam for complex extension UI, not a second product API.** Pi RPC supports `ctx.ui.select`, `confirm`, `input`, and `editor`, but not `ctx.ui.custom()`. When a structured-exchange tool needs a complex shape (multi-select, review-style response, or a deferred multi-question/questionnaire shape) over raw Pi RPC, the tool may call `ctx.ui.editor()` with schema-tagged JSON prefill and validate the returned JSON before producing normal `toolResult.content` plus self-contained `toolResult.details`. A Brunch-aware adapter may render that JSON as a native product form and translate the user response back into Pi's documented `extension_ui_response`; public clients still speak Brunch RPC methods/events, not ad hoc raw Pi RPC extensions. Depends on: D5-L, D19-L, D33-L, D37-L. Supersedes: inventing unsupported Pi RPC command types for Brunch interactions or exposing raw editor JSON as the product UX. - **D13-L — Capture-aware elicitation exchange projection.** Post-exchange capture consumes derived elicitation exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text, linked structured response entries, and/or terminal structured-exchange toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. - **D14-L — `#`-mentions are stable-handle text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable handle (`#A12`, `#I7`, or equivalent node handle) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret the handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. Per-session `(entity_id, snapshotted_lsn)` ledger drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata. -- **D25-L — Elicitation strategies are *lenses* within the `elicitor` agent role, not separate roles or operational modes.** Lens is metadata on elicitor-emitted custom transcript entries (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`, etc.); roles (`elicitor`, `reviewer`, `reconciler`, and any deferred observer/auditor roles) remain orthogonal. The known starter lens set is `step-by-step`, `disambiguate-via-examples`, `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, and `project-requirements-from-upstream`; the catalogue is expected to grow. Capture, review, and future audit routing may filter on lens. Depends on: D12-L, D17-L, D23-L. Supersedes: collapsing strategy and agent role into one vocabulary axis. -- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Single-exchange flows (`step-by-step`, many `disambiguate-via-examples` prompts, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. Batch-proposal flows (`propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`) carry structured entity-draft payloads at proposal time and become durable only through review-set approval. Design/oracle lenses may appear during ordinary elicitation before any commitment posture; later commitment posture changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L. Supersedes: a single uniform "agent asks questions" mental model and the observer-owned extractive vs elicitor-owned generative split as the primary architecture. +- **D25-L — Elicitation strategies are *lenses* within the `elicitor` agent role, not separate roles or operational modes.** Lens is metadata on elicitor-emitted custom transcript entries (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`, etc.); roles (`elicitor`, `reviewer`, `reconciler`, and any deferred observer/auditor roles) remain orthogonal. The catalogue is organized along two axes: *strategies* describe the interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`) while *lenses* describe the topical focus (`intent`, `design`, `oracle`, and future execute-mode lenses `plan`, `sync`, `scope`). The prior lens-catalogue names map to strategy+lens combinations: `propose-scenarios-with-tradeoffs` = propose-graph strategy under intent lens; `propose-design-shapes` = propose-graph under design lens; `propose-oracle-ensembles` = propose-graph under oracle lens; `project-requirements-from-upstream` = project-graph under intent lens; `step-by-step` and `disambiguate-via-examples` = extractive strategies applicable across lenses. The catalogue is expected to grow. Capture, review, and future audit routing may filter on lens. Depends on: D12-L, D17-L, D23-L. Supersedes: collapsing strategy and agent role into one vocabulary axis. +- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation before any commitment posture; later commitment posture changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. - **D30-L — Grounding advances readiness for main elicitation; strategies remain available with honest epistemic signaling.** A minimum grounding bundle — *domain anchor*, *protagonist anchor*, *pain/pull anchor*, *constraint anchor* — establishes the frame required to move the spec from `grounding_onboarding` toward `elicitation_ready`. Lenses and strategies are not refused merely because grounding is thin, but their output resolution and epistemic load must honestly reflect what grounding supports: speculative outputs are visibly hedged and lower-authority, while grounded outputs may drive capture and later review-set projection. Grounding coverage should be explicit in offers/proposals where it affects confidence or gate transitions. Depends on: D26-L, D45-L. Supersedes: gating-by-refusal as a UX move and over-focusing readiness on generative lenses alone. - **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** `brunch.establishment_offer` records the agent's current offer tree and recommended next move as durable transcript state. Ambient chrome or web affordances may render the latest offer, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu. - **D31-L — A four-axis meta-rubric is a soft heuristic for fan-out comparison rubrics across all three flows; not architecturally enforced.** When generating comparison rubrics for fan-out alternatives across candidate-spec, technical-design, and verification-design flows, the elicitor attempts to express each axis in terms of (*legibility / cost-of-knowing*, *failure modes*, *coverage / range*, *commitment*). Project-specific axes are allowed alongside; the meta-frame is dropped when it doesn't fit. The hypothesis (uniform comparison UI across all three flows is more useful than per-flow improvisation) is testable via fixture comparison; promote to schema/UI only if it holds up. Depends on: D25-L, D26-L. Supersedes: a hardcoded per-flow rubric. @@ -244,8 +251,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I3-L | Transcript reload reproduces raw assistant/user payloads plus Brunch session binding, structured elicitation entries, and other custom transcript entries byte-equivalently (modulo timestamps). | covered (M2 JSONL viability round-trip tests) | D6-L | | I4-L | For every `worldUpdate` entry, all named graph items have LSNs strictly greater than the session's pre-update `lastSeenLsn`. | planned (M7 property test) | D6-L, I1-L | | I5-L | For every `brunch.lens_switch` entry and every session/spec binding transition, the session interest set is recomputed before the next agent turn. | planned (M7 property test) | D11-L | -| I6-L | Every reconciliation need has `created_at_lsn ≤` current global LSN; `kind='impasse'` needs reference at least two graph nodes; resolved needs carry a strictly later `resolved_at_lsn`. | planned (M8 property test) | D8-L, I1-L | -| I7-L | Every `framing_as` value belongs to the allowed matrix for that node's base kind. | planned (fixture property check) | D7-L | +| I6-L | Every reconciliation need has `created_at_lsn ≤` current global LSN; its target is exactly one of `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#reconciliationneed--separate-substrate-not-a-graph-edge); resolved needs carry a strictly later `resolved_at_lsn`. | planned (M8 property test) | D8-L, D51-L, I1-L | +| I7-L | ~~Every `framing_as` value belongs to the allowed matrix for that node's base kind.~~ **Retired.** `framing_as` absorbed by D54-L/D56-L node kinds; no node carries a `framing_as` field. | — | D7-L (retired) | | I8-L | Spec selection persists across pi `switchSession` (i.e. `/new`); the selected session file is reopened consistently by headless projection/capture paths; each session has exactly one `brunch.session_binding`, and a session's bound spec never changes. | partially covered (M0 coordinator/TUI boot integration tests + store-only probe checker; M1 no-injected-coordinator capture regression; M2 coordinator-created JSONL reload tests; manual TUI smoke still planned) | D11-L, D21-L | | I9-L | Every `brunch.mention` payload resolves a transcript `#` handle to a stable graph entity id; the ledger never stores title-anchored references or relies on autocomplete popup metadata. | planned (M7 invariant) | D14-L | | I10-L | Structured elicitation prompts/responses live in the Pi transcript when structure is needed; Brunch-supported elicitation exchanges are projected only from linear coordinator-bound sessions, and no parallel canonical chat/turn table carries elicitation state. | covered for projection shape and current read surfaces (M1 exchange projection tests, M2 JSONL/RPC projection tests, M3 canonical Brunch session-envelope validation and explicit custom-entry classifiers) | D12-L, D13-L, D18-L, D24-L | @@ -262,16 +269,20 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I21-L | WebSocket/stdio/TUI client attachment state never becomes the canonical spec/session binding: every session-consuming projection validates the durable `brunch.session_binding`, and write-capable session operations must target an explicit session or future write lease rather than whichever transport connection happens to be open. | partially covered (M3 RPC/WebSocket explicit session projection tests validate durable `brunch.session_binding` for read paths; FE-744 web live-update tests prove WebSocket notifications only invalidate/refetch canonical projection handlers after RPC-originated structured-exchange mutations; future write-lease tests remain planned when web input lands) | D10-L, D19-L, D21-L, D33-L | | I22-L | Brunch TUI startup must not render prior session transcript entries or enter an agent loop until the user has explicitly activated a spec/session decision; creating a new spec implicitly creates its first session, creating a new session for an existing spec lands in a binding-only session, resuming a prior transcript is opt-in, and RPC/headless startup exposes structured initial-selection state rather than invoking TUI picker code. | covered (FE-744 coordinator tests; hierarchical spec/session picker model + component tests; `workspace.selectionState` / `workspace.activate` JSON-RPC contract tests with source assertion that RPC does not import TUI picker code; `src/probes/scripts/verify-startup-no-resume.sh` pty/ANSI-stripped TUI probe oracle proving stale transcript text is absent before explicit activation) | D11-L, D21-L, D22-L, D36-L | | I23-L | Every structured elicitation interaction that owns the response surface persists durable semantic display only through Pi `toolResult` rows rendered by `renderResult`; `renderCall` and live `ctx.ui.*` surfaces are transient. A structured-exchange tuple has a recoverable `present_*` result and, when required, exactly one matching terminal `request_*` result before the next agent turn consumes it. The target details model is checked by `schema` + `v`, `exchange_id`, and `tool_meta`; request outcomes are an exactly-one property-presence union; user-authored text is `comment` and runtime-authored text is `message`; present-side status/kind/expected-request aliases and capture graph payloads are invalid in the Zod-authored schema layer. `toolResult.content` is rich markdown suitable for both TUI transcript display and model context; `toolResult.details` carries structured projection/recovery data. | covered for current FE-744 structured-exchange tools (registered sequential `present_question`, `present_options`, `request_answer`, `request_choice`, and `request_choices`; tests cover non-semantic `renderCall`, markdown `renderResult`, present/request details, unmatched-present recovery, active-vs-stub registry, JSON-editor fallback for multi-choice, terminal `answered`/`cancelled`/`unavailable` projection closure, option content/rationale parity, and same-assistant-message `present_options → request_choice` ordering over a real Pi RPC run. The Zod-authored schema layer is covered by JSON Schema export and drift-rejection tests for present/request/capture details; runtime tools still need a deliberate migration to those exports. `present_review_set`, `present_candidates`, and `request_review` remain named stubs and intentionally unregistered.) | D12-L, D13-L, D17-L, D37-L, D38-L, D41-L | -| I24-L | A Brunch-launched Pi runtime does not load ambient user/project Pi context files, extensions, skills, prompt templates, themes, or behavior-shaping settings unless the Brunch Pi Profile explicitly allows them; Brunch-owned extension-discovered resources are identified as intentional product resources. | planned (sealed-profile audit and resource/settings isolation tests) | D2-L, D39-L | -| I25-L | The active operational mode, role preset/runtime bundle, strategy, and lens are reconstructable from linear transcript entries at turn start; tool gating follows the reconstructed operational mode so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted by the bundle. | planned (runtime-state projection tests plus before-agent-start/tool-policy contract tests) | D17-L, D23-L, D40-L | +| I24-L | A Brunch-launched Pi runtime does not load ambient user/project Pi context files, extensions, skills, prompt templates, themes, or behavior-shaping settings unless the Brunch Pi Profile explicitly allows them; Brunch-owned extension-discovered resources are identified as intentional product resources. | covered for TUI-launch profile boundary by contract tests: ambient resource flags and explicit extension factories are preserved; hostile ambient global/project settings are ignored by the in-memory Brunch settings policy before and after reload; audited Pi settings getters are tracked in `src/brunch-pi-profile.ts`. Subagent subprocess inheritance remains future coverage under I29-L. | D2-L, D39-L | +| I25-L | The active operational mode, role preset/runtime bundle, strategy, and lens are reconstructable from linear transcript entries at turn start; tool gating follows the reconstructed operational mode so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted by the bundle. | covered for the current POC runtime bundle: runtime-state append/project/switch helpers write full `brunch.agent_runtime_state` snapshots to Pi custom entries, init is idempotent, switches carry previous state, malformed/impossible mode-role-strategy-lens entries are ignored or rejected, real SessionManager JSONL reload projects the latest valid state, and session-start/before-agent-start prompt/tool policy derives from the transcript-projected state without extension-local memory. | D17-L, D23-L, D40-L | | I27-L | Session-name generation is best-effort presentation metadata only: lifecycle hooks may append Pi `session_info` entries, but naming failures never block shutdown/session replacement and generated names never mutate spec identity, session binding, or graph truth. | planned (session-lifecycle naming tests with empty transcript/auth failure/success paths; picker projection tests read session names when present) | D6-L, D21-L, D35-L, D42-L | -| I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/tui-client/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | partially covered (structured-exchange schema tests prove Zod parse/export for the acknowledged seam; a grep-based architectural test should still land with M4 for broader schema import boundaries and Drizzle derivation) | D41-L | +| I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/tui-client/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | covered (structured-exchange schema tests prove Zod parse/export; grep-based architectural boundary test in `architecture.test.ts` enforces no direct `db/` imports outside `graph/`; Drizzle derivation via `drizzle-typebox` in `row-schemas.ts`) | D41-L | | I28-L | Auto-compaction output preserves the configured anchor set byte-stable: every entry kind listed in [src/tui-client/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/auto-compaction-anchors.json) is reconstructable post-compaction according to its `select` rule (`first | latest | active-leaves | all-unresolved`); LLM-generated narrative summary never replaces or rephrases preserved-anchor content; extension failure falls through to Pi default compaction rather than dropping anchors silently. | planned (compaction round-trip property tests at M9 plus inner-loop anchor-rendering unit tests and TypeBox schema validation of the anchor config) | D43-L; R15, R13; I3-L, I4-L, I8-L, I12-L | | I29-L | Subagent subprocesses inherit Brunch Pi Profile sealing: every `subagent` tool invocation spawns `pi --mode json -p --no-session --no-skills --no-extensions` with an explicit per-agent tool allowlist and per-agent model; subagents never load ambient user/project `.pi/` skills, prompts, themes, extensions, context files, or behavior-shaping settings; subagents never gain direct access to the parent's `CommandExecutor`, Brunch RPC handlers, or graph persistence; subagent results return to the main agent only as tool result content (no side-effect transcript writes). | planned (subagent subprocess argv tests; isolation audit asserting absent ambient-resource leakage; tool-allowlist conformance test per starter agent) | D2-L, D39-L, D44-L; I2-L, I11-L, I24-L | | I30-L | Elicitor post-exchange capture only commits high-confidence extractive facts, concrete reconciliation needs, and justified spec grade/posture updates; low-confidence implications remain in structured-exchange preface/question material and do not become graph truth until clarified, accepted, or explicitly escalated. | planned (M5 capture fixtures comparing committed graph facts and preface-only interpretations against transcript evidence) | D18-L, D47-L; A22-L | | I31-L | `readiness_grade` is a forward gate, not a workflow location: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable; all grade/posture mutations route through `CommandExecutor` and carry provenance. | planned (M4 schema/command tests for spec row updates; M5 prompt/tool-policy tests for grade-gated availability) | D20-L, D45-L | | I32-L | Public RPC structured-exchange driving never requires a client to speak raw Pi RPC: after Brunch method discovery and workspace/spec/session activation, each pending assistant-originated exchange is answered exactly once through `elicitation.respond`, and the deterministic permutation run produces linear Pi JSONL whose transcript display and elicitation-exchange projections preserve the same prompt/answer/status/comment artifacts as the equivalent TUI structured-exchange path. | covered for deterministic FE-744 parity (`rpc.discover` contract tests, pending/respond lifecycle tests, current public-RPC structured-exchange permutations, terminal non-answered status handling, option content/rationale parity, no repeated deterministic prompts, and transcript/projection oracle in `src/probes/public-rpc-parity-proof.ts`) | R11, R16, R17, R24, R27, R28; D5-L, D12-L, D37-L, D48-L, D49-L | | I33-L | `capture_*` analysis entries are transcript evidence only: they persist as Brunch structured-exchange `toolResult` rows, are included by Brunch-semantic transcript renderers, are hidden or collapsed in TUI display, and never mutate graph truth or bypass `CommandExecutor`. | partially covered (minimum capture details schemas parse/export and reject graph payload fields; future runtime capture-analysis schema/rendering tests plus transcript renderer fixtures still need to prove persisted result rendering and TUI hide/collapse behavior; later graph-capture fixtures compare analysis candidates against committed graph mutations) | D17-L, D18-L, D37-L, D47-L, D50-L; I2-L, I11-L, I23-L, I30-L | +| I34-L | `commitGraph` batch validation is all-or-nothing: if any node or edge in the batch is structurally illegal, the entire batch is rejected and no partial state is persisted; the agent receives diagnostics sufficient for bounded self-correction retry. | covered (22 tests in `command-executor.test.ts` — edge failure rolls back nodes, mixed-batch rejection, diagnostic sufficiency) | D53-L; I1-L, I11-L | +| I35-L | Graph context snapshots support multiple detail levels: a cursory/compact full-graph overview for orientation, and detailed node-neighborhood snapshots with configurable hop depth for focused work. Context builders in `agents/contexts/` orchestrate which level to inject based on mode/role/strategy/lens/grade. | partially covered (`getGraphOverview` + `getNodeNeighborhood` in `snapshot.ts` with 10 tests; context-builder integration deferred to M5) | D52-L, D53-L | +| I36-L | Node `kind` is drawn from a per-plane closed enum structurally validated by the `CommandExecutor`; the intent kind category (basic / structural / reasoning) is a pure function of `kind` and is never stored on the node. | covered (CommandExecutor rejects invalid kind-for-plane; `intentKindCategory` is pure derivation with exhaustive switch; tests in `command-executor.test.ts`) | D54-L, D56-L | +| I37-L | `detail` is per-kind validated by the `CommandExecutor`: `decision` and `term` nodes REQUIRE `detail` with their respective sub-schemas; all other kinds must omit `detail`; unknown fields in `detail` are rejected. | covered (detail-required/prohibited/shape tests in `command-executor.test.ts`) | D54-L | ## Future Direction Register @@ -295,7 +306,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules. The initial private prompt-pack topology lives under `src/tui-client/.pi/context/`, with deterministic composition through `compose-brunch-prompt.ts` and future dynamic context renderers under `context/builders/`. +- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules. The target topology per D52-L is `src/agents/` organized by axis: `modes/` (operational mode prompts and rules), `strategies/` (interaction-shape prompts per strategy), `lenses/` (topical-focus prompts per lens), and `contexts/` (snapshot orchestration functions that import from `graph/` and `session/`). Prompt composition lives in `agents/compose.ts`; state definitions (valid mode/role/strategy/lens combinations) in `agents/state.ts`. The current `src/tui-client/.pi/context/` layout migrates to this structure. - Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` and `elicitation_posture` live on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade/posture diverge. Before these fields drive hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade/posture. - Core role/lens prompting should usually be product prompt packs rather than Pi skills. Pi skills remain available as Brunch-owned explicit resources when progressive disclosure is the right mechanism, but they are not the primary authority for operational mode/tool policy. @@ -307,7 +318,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Vocabulary evolution - Whether public graph commands eventually split from one `graph.*` umbrella into `intent.*` / `oracle.*` / `design.*` / `plan.*` namespaces is deferred; current posture is unified `graph.*` for the POC. -- Whether `framing_as` values graduate to first-class node kinds (seam-extensions Open Question #8) is deferred until fixture pressure shows the need. +- ~~Whether `framing_as` values graduate to first-class node kinds~~ — resolved: `framing_as` retired, absorbed by `thesis`, `term`, `constraint.subtype`, and `goal` (D54-L, D56-L). +- `posture` is a spec-level property set for now; whether it earns a graph node kind is deferred until product pressure shows that per-subsystem or per-phase posture declarations need graph-native representation. ### Thin transport/read posture @@ -344,15 +356,15 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Operational mode** | A top-level Brunch authority/tooling posture such as `elicit` or future `execute`. It determines what kind of work is allowed and which tools/prompt posture are available. Distinct from Pi's transport mode concept. | | **Agent role** | A worker identity within an operational mode. Top-level roles drive the main turn (`elicitor`, future `executor/orchestrator`); side roles run async/advisory or delegated work (`reviewer`, `reconciler`, deferred observer/auditor, future `scout` / `researcher`). | | **Runtime bundle / role preset** | The transcript-backed Brunch selection that derives active operational mode, top-level role, model, thinking level, prompt packs, allowed strategies/lenses, and tool policy. Commands switch bundles instead of mutating hidden extension memory. | -| **Strategy** | A conversation or work tactic selected within the active runtime bundle. Strategies control interaction plan; lenses control interpretive/extraction/review framing. | -| **Lens** | A narrower interpretive, extraction, or review framing applied within a role/strategy, such as technical-design, verification-design, or disambiguation. Lenses may eventually be driven by Brunch-owned prompt packs or skills. | +| **Strategy** | An interaction-shape tactic within the active runtime bundle: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges from existing graph via review-set). Strategies determine the commitment mechanism (D26-L). | +| **Lens** | A topical-focus framing applied within a role/strategy: `intent`, `design`, `oracle` for elicitation mode; `plan`, `sync`, `scope` for future execute mode. Strategy+lens pairs map to the prior lens-catalogue names (D25-L). | | **Brunch Pi Profile** | The sealed programmatic wrapper around embedded Pi: settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. It allows Brunch-owned resources while suppressing ambient `.pi/` behavior. | | **Prompt pack** | A Brunch-owned prompt fragment selected by operational mode, role preset, strategy, lens, readiness grade, or elicitation posture. Prompt packs compose at turn boundaries; they are product control-plane state, not ambient Pi prompt templates. | | **Readiness grade** | Spec-owned forward gate stored on the spec row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, commitment review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | | **Elicitation posture** | Spec-owned current stance inside `elicit`: `gathering | refining | pinning`. Semi-independent from readiness grade; a high-grade spec may still return to gathering/refining when new ambiguity appears. | | **Commitment focus** | Optional/deferred target for `pinning` posture (`design | oracle`) if active review-set state and missing-commitment analysis cannot make the focus obvious. Not required as canonical state now. | | **Coherence** | Bounded product-visible verdict over whether the current spec graph is structurally legal and free of known unresolved contradictions/gaps at the current maturity. It is backed by reconciliation needs and remains intentionally narrower than a general judgment that the whole idea is good or complete. | -| **Structural legality** | Synchronous schema/ontology validity of graph mutations: allowed node/edge shapes, required fields, framing matrix, and transaction invariants. Structural legality can fail even before semantic coherence is evaluated. | +| **Structural legality** | Synchronous schema/ontology validity of graph mutations: edge categories from the closed set in `docs/design/GRAPH_MODEL.md`, per-category stance/cardinality/acyclicity rules, immutable accepted-edge identity (`category`, `sourceId`, `targetId`, `stance`), per-plane closed node `kind` enums, required `detail` sub-schemas for `decision`/`term`, `constraint.subtype` enum, and transaction invariants. Structural legality can fail even before semantic coherence is evaluated. | | **Print snapshot** | The M1 meaning of the print transport mode: boot the Brunch host, resolve workspace/spec/session state through the coordinator, render product-shaped state, and exit without running an agent turn. | | **Workspace** | The current working directory where the Brunch CLI was invoked. It scopes `.brunch/` state for the launch context. It is not user-created, not selectable within the dialog, and there is only one active workspace per Brunch process. The UI may display a project identity/name derived from cwd-local manifests or directory basename, but that name labels the cwd; it does not create a separate workspace object. | | **Spec / specification** | The user-created specification container within a workspace, identified by its intent-graph root. Multiple specs may coexist under one workspace. A spec contains sessions and the graph data gathered through those sessions (intent nodes, design nodes, oracle/plan data as they land). Future plan-execution mode operates on a selected spec. | @@ -370,10 +382,13 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Plan graph** | Milestone/frontier/slice delivery claims accountable to intent, oracle, and design. Stubbed in POC. | | **LSN** | Log Sequence Number. A single monotonic counter, one-LSN-per-commit, shared by the change log, graph-node versions, and reconciliation needs. | | **Change log** | The audit trail of graph mutations. Authoritative for replay, `worldUpdate` synthesis, and reconciliation-need ordering. | -| **Reconciliation need** | First-class record of an open impasse, gap, contradiction, or process debt; carries `created_at_lsn`, optional `resolved_at_lsn`, `concerns` edges to graph nodes. Routine async jobs are not reconciliation needs unless they surface semantic work to resolve. | +| **Reconciliation need** | First-class record of an open impasse, gap, contradiction, or process debt; carries `created_at_lsn`, optional `resolved_at_lsn`, and a target that is exactly one of `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` per `docs/design/GRAPH_MODEL.md`. Recon-needs are a separate substrate, not graph edges (no `concerns`-edge wiring). Routine async jobs are not reconciliation needs unless they surface semantic work to resolve. | | **Coherence verdict** | Per-spec product state (`coherent` / `incoherent`) emitted by validators and visible to both UI and agent. | | **Command layer** | The single Brunch-owned mutation surface. Validates, gates concurrency, audits, emits events, triggers coherence. Its public mutation entry point is the `CommandExecutor`, not direct ORM calls or caller-side authority gates. | | **Command executor** | The deep module that accepts Brunch product commands plus execution context and returns structured command results (`ok`, `needs_human`, `policy_blocked`, `version_conflict`, `structural_illegal`). It hides attribution, minimal pre-M6 authority classification, validation, transaction, LSN, change-log, and coherence-trigger mechanics from callers. | +| **commitGraph** | Single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references. One tool call, one LSN, all-or-nothing (I34-L). The load-bearing tool for the `propose-graph` strategy's direct-commit path (D53-L). | +| **propose-graph** | Elicitor strategy for generative lenses where the agent proposes a novel coherent subgraph. The concept is presented to the user with rubric axes, choices, and recommendation via structured exchange; upon acceptance the agent generates and persists the full subgraph through `commitGraph` without intermediate entity-level review (D26-L, D53-L). The hardest thing to get structurally legal and the primary proof target for A14-L. | +| **project-graph** | Elicitor strategy for deriving nodes and edges from existing graph truth (e.g. projecting requirements from upstream goals/constraints). Uses review-set commitment (D27-L). Extractive rather than inventive; lower structural-legality risk than propose-graph. | | **Brunch public RPC surface** | The one product-facing JSON-RPC surface exposed over stdio, WebSocket, and in-process handlers. Product clients use this surface for workspace, session, graph, coherence, command, agent, and elicitation behavior; raw Pi RPC is hidden behind adapters when needed. | | **RPC discovery** | Brunch-owned `rpc.discover` method output: public method names, descriptions, parameter/result schemas, and examples for the current Brunch host. It is distinct from Pi `get_commands`, which only lists slash commands/prompt templates/skills invokable through Pi's `prompt` command. | | **RPC method family** | A named group of Brunch JSON-RPC methods (`rpc.*`, `workspace.*`, `session.*`, `elicitation.*`, `graph.*`, `coherence.*`, `command.*`) that exposes product behavior through stdio, WebSocket, or in-process handler calls without creating a second public API surface. | @@ -414,7 +429,14 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Mention ledger** | Per-session `(entity_id, snapshotted_lsn)` record driving discretionary staleness hints when an entity has changed since the agent last saw it. | | **Authority** | Source of a node's claim: `stakeholder | technical | external | derived`. | | **Epistemic status** | Confidence basis: `observed | asserted | assumed | inferred`. Like `authority`, this is a context-shaping label for attention, grouping, and compression rather than a complete theory of truth. | -| **Framing-as** | Orthogonal modality classifying a node's product role (e.g. `problem`, `persona`, `non_goal`) within an allowed matrix. | +| **Framing-as** | ~~Orthogonal modality classifying a node's product role.~~ **Retired.** Absorbed by `thesis`, `term`, `constraint.subtype`, and `goal` (D54-L, D56-L). | +| **Thesis** | A first-class intent node kind (`kind: "thesis"`). A chosen position or bet about the product — falsifiable, carries "what/who/why/for whom" material (La Carte Blanche style). Not a requirement (it's a bet, not a need), not a goal (it's falsifiable, not aspirational), not an assumption (it's a chosen position, not a dependency). Natural edge relationships: criteria and evidence witness for/against a thesis via `proof` edges. | +| **Term** | A first-class intent node kind (`kind: "term"`). A canonical naming commitment for ubiquitous language and conceptual consistency. Requires `detail: { definition, aliases? }`. Participates in graph edges: downstream nodes may `dependency`-depend on the term's definition; a term may `boundary`-scope what counts as X; a newer term may `supersession`-replace a prior term. | +| **Node source** | Free-form string on `GraphNode.source` for epistemic attribution (e.g. "stakeholder", "regulatory", "derived"). Convention by prompt, not structural validation. Exists for context-snapshot enrichment — rendered back into sparse text in prompt snapshots, not used for policy or filtering. Not applicable to edges. | +| **Node detail** | Optional JSON column on `GraphNode.detail` with per-kind validated sub-structures. `decision` requires `{ chosen_option, rejected, rationale }`; `term` requires `{ definition, aliases? }`; `constraint` may carry `{ subtype }`. All other kinds omit `detail`. | +| **Context (node kind)** | A first-class intent node kind (`kind: "context"`). A descriptive claim about the environment — observed facts that color interpretation without driving decisions directly. Last-resort basic bucket: before filing as context, check the promotion heuristic (must be true for success → requirement/invariant; limits solutions → constraint; may be false → assumption; chooses among alternatives → decision; bet about users/market → thesis). | +| **Intent kind category** | Derived grouping of intent node kinds: `basic` (goal, thesis, term, context), `structural` (requirement, assumption, constraint, invariant), `reasoning` (decision, criterion, example). A pure function of `kind`, not stored. Maps to spec-grade progression — grounding gate requires satisficing threshold of basic-category nodes. | +| **Posture** | A spec-level property set declaring project epistemic/strategic stance (certainty, stakes, audience, horizon, migration, sourcing). Not a graph node kind in the POC; spec-level rather than graph-level. Grounding elicitation interweaves basic intent nodes with posture establishment. | | **Kernel** | A behavioural elicitation pattern from `docs/design/BEHAVIORAL_KERNELS.md` (state/lifecycle, containment, concurrency, etc.). | | **Probe run** | A scripted or executable check of a Brunch seam that drives the public product surface and persists reviewable artifacts under `.fixtures/runs///`. | | **Transcript artifact** | The durable transcript evidence for a probe run, usually `session.jsonl` plus a Brunch-semantic `transcript.md`; reports explain the oracle, but transcript artifacts remain the evidence. | @@ -524,7 +546,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I4-L | M7 generated LSN/change traces and paired-session fixture assertions. | | I5-L | M7 property tests over binding/lens transitions and interest-set recomputation. | | I6-L | M4/M8 reconciliation-need property tests and contradictory-requirements fixture. | -| I7-L | M4+ schema/property tests over framing matrix plus brief fixture assertions. | +| I7-L | ~~M4+ framing matrix tests.~~ **Retired** with `framing_as` (D54-L, D56-L). | | I8-L | M0 probe oracle plus M2 coordinator-created JSONL reload tests. | | I9-L | M7 mention parser/ledger unit tests and staleness property tests. | | I10-L | M1/M2 exchange projection tests, linear transcript validation, and no chat/turn architectural test. | @@ -550,6 +572,8 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I31-L | M4/M5 spec-row command tests for grade/posture updates plus prompt/tool-policy tests proving grade gates unlock later actions without disabling gathering/refinement. | | I32-L | FE-744 public-RPC structured-exchange parity proof: `rpc.discover` contract tests, pending/respond lifecycle tests, deterministic permutation run over Brunch JSON-RPC only, no repeated deterministic prompts, and parity assertions over the resulting Pi JSONL, transcript display, and elicitation-exchange projections. | | I33-L | Current schema tests cover minimum no-graph `capture_*` details and reject graph payload fields. Future capture-analysis runtime tests must still cover persisted result rendering, no graph-write side effects, Brunch-semantic transcript inclusion, and hidden/collapsed TUI rendering fallback. | +| I36-L | M4 per-plane kind enum validation tests in CommandExecutor; kind-to-category derivation unit tests proving pure function parity with GRAPH_MODEL.md table. | +| I37-L | M4 node-creation tests: decision/term rejected without detail; constraint accepted with or without detail; other kinds rejected with detail; unknown detail fields rejected. | ### Design Notes diff --git a/package-lock.json b/package-lock.json index 146e1a327..475df253b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,13 +21,19 @@ "brunch-next": "bin/brunch.js" }, "devDependencies": { + "@sinclair/typebox": "^0.34.14", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.7.0", + "better-sqlite3": "^12.8.0", + "drizzle-kit": "^0.31.10", + "drizzle-orm": "^0.45.2", + "drizzle-typebox": "^0.3.3", "jsdom": "^29.1.1", "oxfmt": "^0.2.0", "oxlint": "^0.18.0", @@ -988,6 +994,13 @@ "node": ">=20.19.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@earendil-works/pi-agent-core": { "version": "0.75.3", "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", @@ -1076,27 +1089,22 @@ "koffi": "2.16.2" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], + "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", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], @@ -1107,13 +1115,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], @@ -1124,13 +1132,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], @@ -1141,13 +1149,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -1158,13 +1166,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -1175,13 +1183,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], @@ -1192,13 +1200,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], @@ -1209,13 +1217,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], @@ -1226,13 +1234,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], @@ -1243,13 +1251,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], @@ -1260,13 +1268,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], @@ -1277,13 +1285,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], @@ -1294,13 +1302,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], @@ -1311,13 +1319,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], @@ -1328,13 +1336,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], @@ -1345,13 +1353,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -1362,15 +1370,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -1379,13 +1387,13 @@ "netbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], @@ -1393,16 +1401,33 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ "arm64" ], @@ -1410,16 +1435,33 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -1427,50 +1469,100 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/android-arm": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/android-arm64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -1478,359 +1570,288 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/android-x64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "node": ">=18" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", - "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", - "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.6", - "@mariozechner/clipboard-darwin-universal": "0.3.6", - "@mariozechner/clipboard-darwin-x64": "0.3.6", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-musl": "0.3.6", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", - "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", - "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", - "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ - "x64" + "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", - "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ - "arm64" - ], - "libc": [ - "glibc" + "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", - "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ - "arm64" - ], - "libc": [ - "musl" + "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", - "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", - "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ - "x64" - ], - "libc": [ - "glibc" + "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", - "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", - "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "netbsd" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", - "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", - "cpu": [ + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "netbsd" ], "engines": { - "node": ">= 10" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" + "node": ">=18" } }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@oxfmt/darwin-arm64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/darwin-arm64/-/darwin-arm64-0.2.0.tgz", - "integrity": "sha512-NK7iEPqRovUvKac+4dn2ui8v5Y5q6UJ9v4z5Zjr5lmEzTlBwXToP3TwY75IAaCYeu0g8Es7ToJpS7qCQuxUhuA==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -1838,13 +1859,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@oxfmt/darwin-x64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/darwin-x64/-/darwin-x64-0.2.0.tgz", - "integrity": "sha512-eXDgT+DbIMnA3sWE+w38rOvXaxkP4RvHi41rPpWb9XoRGbM+tOo0c8RYqCIX39n7PizOlbF/xUBh9nMhiW01wQ==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -1852,95 +1876,84 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] - }, - "node_modules/@oxfmt/linux-arm64-gnu": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-arm64-gnu/-/linux-arm64-gnu-0.2.0.tgz", - "integrity": "sha512-h4mw9/Lck5X/SU1RPE26VraoBJgn9SOoLl8GnaYvtmRBFHK0v+2KpnBSwMWeiaIxCdJSsrVc9mpKSOcPsen7Cg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" + "openbsd" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@oxfmt/linux-arm64-musl": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-arm64-musl/-/linux-arm64-musl-0.2.0.tgz", - "integrity": "sha512-kDZ/kVCgh9kSczMH2gPXZs+rsY/VcCQ1BoQnND8D3v1Le91LceJ5s/a4XBBA3ADI3NiPdqNmK2ssaxY4BfgXDg==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@oxfmt/linux-x64-gnu": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-x64-gnu/-/linux-x64-gnu-0.2.0.tgz", - "integrity": "sha512-qvf6cNuf4z3yjzjSvg4Jwr88jDyeFaYPU+4dBWVr6fdWhAA+BIuIvurJWYQGvWNNSkobVWH3A7m+wp+h5fNsSg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@oxfmt/linux-x64-musl": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-x64-musl/-/linux-x64-musl-0.2.0.tgz", - "integrity": "sha512-D0rw4BVLHEbtMB6p9e+Ph7Y/56oD1DRP4Qcbbv1B1w0kJanWBB3vMH+v2rqgeYsErqJOtLpYRkPFmJSU9In2HA==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@oxfmt/win32-arm64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/win32-arm64/-/win32-arm64-0.2.0.tgz", - "integrity": "sha512-QM0fP8YUvBNyuNa6d+jNonV34A6zGvvQ2mX93wnERpX25jIbFKljKiKDiWo2M1nUsW8zL2z4ESEeaLO9bx8djg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@oxfmt/win32-x64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/win32-x64/-/win32-x64-0.2.0.tgz", - "integrity": "sha512-WkFXliDbwFiziBSfvWXeM0Y3x07Kxxrl39kgsp+N3n5QgzsM8q6qe094+OW+hItDQ+9SSd2y9u231sMCsKxlog==", + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -1949,61 +1962,196 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@oxlint/darwin-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-0.18.1.tgz", - "integrity": "sha512-FqDrcQJmEGNkgmZgI4wbCrGyJl1tiRZa3udxvyYaXag8W80A0zLFNCyWVvHIgUJ0DHlZjRc7O72xUGjiyvQrqQ==", - "cpu": [ - "arm64" ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } }, - "node_modules/@oxlint/darwin-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-0.18.1.tgz", - "integrity": "sha512-YUcyWBJdNuMcJxAwdV/i25/kvnKrVsA+vLn7SsL87cAwiD//rqGdOixk0r8sKUYa71Kx3h0Fg2ToUOjdE6ddYw==", - "cpu": [ - "x64" - ], + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } }, - "node_modules/@oxlint/linux-arm64-gnu": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-0.18.1.tgz", - "integrity": "sha512-ol3jhmUv5VI/omMrt6DkwY/jVTSVJlflFyU1SnSb/BuVVf3TyBiCHmZ4wVtcrcT5k3sWjrvYWw2kSozvmuE4tg==", - "cpu": [ - "arm64" - ], + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } }, - "node_modules/@oxlint/linux-arm64-musl": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-0.18.1.tgz", - "integrity": "sha512-iKDj1ZwlU4KpXuIL1qkVP6NJzri2VSJreqXCIAe1Bf5RZXMAGSO3xjldgiX+HBvFOKSBIarLcqONYDbYco9uaQ==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.6", + "@mariozechner/clipboard-darwin-universal": "0.3.6", + "@mariozechner/clipboard-darwin-x64": "0.3.6", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-musl": "0.3.6", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + } + }, + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -2011,16 +2159,37 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@oxlint/linux-x64-gnu": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-0.18.1.tgz", - "integrity": "sha512-A3g+fZhlOivUdK7xU/IrbhBcMHig5GLrfMX0HYjXL1fiSqKYu9n1o1p42WpT6KfPL3L2uncSg/iyg7hspcN6qA==", + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -2028,16 +2197,18 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@oxlint/linux-x64-musl": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-0.18.1.tgz", - "integrity": "sha512-LA02SdATWZEZBy8ZZpR2GlUbDg7+Jq1/WKkywMXqxdClkcoyyFozj8aQD2iTMKELSra4OSyqqZpOYroqjSSKmw==", + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -2045,138 +2216,70 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@oxlint/win32-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-0.18.1.tgz", - "integrity": "sha512-FNL+OxDflqLGXRgLxfBM/X4RnLYgtOKTsb1mNSqsjSCEfUi1Oqivh7KvZ09IfAMZeJ85/fL6EI6hSOyY7nNYUg==", + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@oxlint/win32-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-0.18.1.tgz", - "integrity": "sha512-W+aVE9Siqs6Oe3NDaDOTTOYsN9X3znl+whfqWK1EcLpqJXX1kdB8Hf45HkGjqnHoFoP96GRgUnXQHQvmUybjvg==", + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", "dependencies": { - "@protobufjs/aspromise": "^1.1.1" + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@oxfmt/darwin-arm64": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/darwin-arm64/-/darwin-arm64-0.2.0.tgz", + "integrity": "sha512-NK7iEPqRovUvKac+4dn2ui8v5Y5q6UJ9v4z5Zjr5lmEzTlBwXToP3TwY75IAaCYeu0g8Es7ToJpS7qCQuxUhuA==", "cpu": [ "arm64" ], @@ -2187,10 +2290,10 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@oxfmt/darwin-x64": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/darwin-x64/-/darwin-x64-0.2.0.tgz", + "integrity": "sha512-eXDgT+DbIMnA3sWE+w38rOvXaxkP4RvHi41rPpWb9XoRGbM+tOo0c8RYqCIX39n7PizOlbF/xUBh9nMhiW01wQ==", "cpu": [ "x64" ], @@ -2201,40 +2304,46 @@ "darwin" ] }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "node_modules/@oxfmt/linux-arm64-gnu": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/linux-arm64-gnu/-/linux-arm64-gnu-0.2.0.tgz", + "integrity": "sha512-h4mw9/Lck5X/SU1RPE26VraoBJgn9SOoLl8GnaYvtmRBFHK0v+2KpnBSwMWeiaIxCdJSsrVc9mpKSOcPsen7Cg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@oxfmt/linux-arm64-musl": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/linux-arm64-musl/-/linux-arm64-musl-0.2.0.tgz", + "integrity": "sha512-kDZ/kVCgh9kSczMH2gPXZs+rsY/VcCQ1BoQnND8D3v1Le91LceJ5s/a4XBBA3ADI3NiPdqNmK2ssaxY4BfgXDg==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "node_modules/@oxfmt/linux-x64-gnu": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/linux-x64-gnu/-/linux-x64-gnu-0.2.0.tgz", + "integrity": "sha512-qvf6cNuf4z3yjzjSvg4Jwr88jDyeFaYPU+4dBWVr6fdWhAA+BIuIvurJWYQGvWNNSkobVWH3A7m+wp+h5fNsSg==", "cpu": [ - "arm" + "x64" ], "dev": true, "libc": [ @@ -2246,12 +2355,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "node_modules/@oxfmt/linux-x64-musl": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/linux-x64-musl/-/linux-x64-musl-0.2.0.tgz", + "integrity": "sha512-D0rw4BVLHEbtMB6p9e+Ph7Y/56oD1DRP4Qcbbv1B1w0kJanWBB3vMH+v2rqgeYsErqJOtLpYRkPFmJSU9In2HA==", "cpu": [ - "arm" + "x64" ], "dev": true, "libc": [ @@ -2263,80 +2372,68 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "node_modules/@oxfmt/win32-arm64": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/win32-arm64/-/win32-arm64-0.2.0.tgz", + "integrity": "sha512-QM0fP8YUvBNyuNa6d+jNonV34A6zGvvQ2mX93wnERpX25jIbFKljKiKDiWo2M1nUsW8zL2z4ESEeaLO9bx8djg==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@oxfmt/win32-x64": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oxfmt/win32-x64/-/win32-x64-0.2.0.tgz", + "integrity": "sha512-WkFXliDbwFiziBSfvWXeM0Y3x07Kxxrl39kgsp+N3n5QgzsM8q6qe094+OW+hItDQ+9SSd2y9u231sMCsKxlog==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "node_modules/@oxlint/darwin-arm64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-0.18.1.tgz", + "integrity": "sha512-FqDrcQJmEGNkgmZgI4wbCrGyJl1tiRZa3udxvyYaXag8W80A0zLFNCyWVvHIgUJ0DHlZjRc7O72xUGjiyvQrqQ==", "cpu": [ - "loong64" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ] }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "node_modules/@oxlint/darwin-x64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-0.18.1.tgz", + "integrity": "sha512-YUcyWBJdNuMcJxAwdV/i25/kvnKrVsA+vLn7SsL87cAwiD//rqGdOixk0r8sKUYa71Kx3h0Fg2ToUOjdE6ddYw==", "cpu": [ - "loong64" + "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ] }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "node_modules/@oxlint/linux-arm64-gnu": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-0.18.1.tgz", + "integrity": "sha512-ol3jhmUv5VI/omMrt6DkwY/jVTSVJlflFyU1SnSb/BuVVf3TyBiCHmZ4wVtcrcT5k3sWjrvYWw2kSozvmuE4tg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "libc": [ @@ -2348,12 +2445,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "node_modules/@oxlint/linux-arm64-musl": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-0.18.1.tgz", + "integrity": "sha512-iKDj1ZwlU4KpXuIL1qkVP6NJzri2VSJreqXCIAe1Bf5RZXMAGSO3xjldgiX+HBvFOKSBIarLcqONYDbYco9uaQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "libc": [ @@ -2365,12 +2462,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "node_modules/@oxlint/linux-x64-gnu": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-0.18.1.tgz", + "integrity": "sha512-A3g+fZhlOivUdK7xU/IrbhBcMHig5GLrfMX0HYjXL1fiSqKYu9n1o1p42WpT6KfPL3L2uncSg/iyg7hspcN6qA==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "libc": [ @@ -2382,12 +2479,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "node_modules/@oxlint/linux-x64-musl": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-0.18.1.tgz", + "integrity": "sha512-LA02SdATWZEZBy8ZZpR2GlUbDg7+Jq1/WKkywMXqxdClkcoyyFozj8aQD2iTMKELSra4OSyqqZpOYroqjSSKmw==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "libc": [ @@ -2399,75 +2496,136 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@oxlint/win32-arm64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-0.18.1.tgz", + "integrity": "sha512-FNL+OxDflqLGXRgLxfBM/X4RnLYgtOKTsb1mNSqsjSCEfUi1Oqivh7KvZ09IfAMZeJ85/fL6EI6hSOyY7nNYUg==", "cpu": [ - "s390x" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@oxlint/win32-x64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-0.18.1.tgz", + "integrity": "sha512-W+aVE9Siqs6Oe3NDaDOTTOYsN9X3znl+whfqWK1EcLpqJXX1kdB8Hf45HkGjqnHoFoP96GRgUnXQHQvmUybjvg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ] }, - "node_modules/@rollup/rollup-openbsd-x64": { + "node_modules/@rollup/rollup-android-arm64": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ] }, - "node_modules/@rollup/rollup-openharmony-arm64": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -2475,41 +2633,41 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "darwin" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { + "node_modules/@rollup/rollup-darwin-x64": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ] }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { + "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "freebsd" ] }, - "node_modules/@rollup/rollup-win32-x64-gnu": { + "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -2517,916 +2675,1973 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "freebsd" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ - "x64" + "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0" + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@smithy/core": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", - "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", - "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", - "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.14", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.14.tgz", + "integrity": "sha512-TJ7Al17j3+by5y2QkTLcF/oBVMbgXBhILVgi9PuwpxQVZZvGh5BFRzWbJPmZVNKpbRLjuMzFuRwR+tdFPqCkvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { "node": ">=14.0.0" } }, - "node_modules/@smithy/node-http-handler": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", - "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", - "license": "Apache-2.0", + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tanstack/history": { + "version": "1.162.0", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", + "integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.11", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.11.tgz", + "integrity": "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.11.tgz", + "integrity": "sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.170.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.6.tgz", + "integrity": "sha512-tGQkOjcMESBbfw+iz9zE/ivcuw4D2zdhW8PA4wMmpOyA2bLiqpc6bg5MnJPxamdXoO3GZBiHYOOoEwi5qxpPgA==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.171.4", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.9.3", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.171.4", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.4.tgz", + "integrity": "sha512-LU9YuVdgaP+h4MEXRvokyjIEelulylgsromHMfYwVfgo1PF9oJP3NHyy7qtjxGLJq6zoZMCfoa1frDJlPo7I8g==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "cookie-es": "^3.0.0", + "seroval": "^1.5.4", + "seroval-plugins": "^1.5.4" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "license": "MIT", + "funding": { + "type": "github", + "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", + "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/@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" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=18.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@smithy/signature-v4": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", - "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", - "license": "Apache-2.0", + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" }, - "engines": { - "node": ">=18.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", - "license": "Apache-2.0", + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" }, - "engines": { - "node": ">=18.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" + "tinyspy": "^3.0.2" }, - "engines": { - "node": ">=14.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">= 14" } }, - "node_modules/@tanstack/history": { - "version": "1.162.0", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", - "integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=20.19" + "node": ">=8" + } + }, + "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", + "engines": { + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@tanstack/query-core": { - "version": "5.100.11", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.11.tgz", - "integrity": "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==", + "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", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "engines": { + "node": ">=12" } }, - "node_modules/@tanstack/react-query": { - "version": "5.100.11", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.11.tgz", - "integrity": "sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.100.11" + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" }, - "peerDependencies": { - "react": "^18 || ^19" + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" } }, - "node_modules/@tanstack/react-router": { - "version": "1.170.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.6.tgz", - "integrity": "sha512-tGQkOjcMESBbfw+iz9zE/ivcuw4D2zdhW8PA4wMmpOyA2bLiqpc6bg5MnJPxamdXoO3GZBiHYOOoEwi5qxpPgA==", + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "license": "MIT", "dependencies": { - "@tanstack/history": "1.162.0", - "@tanstack/react-store": "^0.9.3", - "@tanstack/router-core": "1.171.4", - "isbot": "^5.1.22" - }, - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "node_modules/@tanstack/react-store": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", - "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.9.3", - "use-sync-external-store": "^1.6.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@tanstack/router-core": { - "version": "1.171.4", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.4.tgz", - "integrity": "sha512-LU9YuVdgaP+h4MEXRvokyjIEelulylgsromHMfYwVfgo1PF9oJP3NHyy7qtjxGLJq6zoZMCfoa1frDJlPo7I8g==", + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "@tanstack/history": "1.162.0", - "cookie-es": "^3.0.0", - "seroval": "^1.5.4", - "seroval-plugins": "^1.5.4" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, - "engines": { - "node": ">=20.19" + "bin": { + "browserslist": "cli.js" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@tanstack/store": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", - "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, - "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==", + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", - "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": ">=8" } }, - "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==", + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "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 - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=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==", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 16" + } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "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==", "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" + "engines": { + "node": ">= 12" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" + "engines": { + "node": ">=6" } }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "engines": { + "node": ">=4.0.0" } }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "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==", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "node": ">=8" } }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "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" + }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "bin": { + "drizzle-kit": "bin.cjs" } }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 14" + "node": ">=18" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", - "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=18" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MIT", + "optional": true, + "os": [ + "netbsd" ], - "license": "CC-BY-4.0" + "engines": { + "node": ">=18" + } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { "node": ">=18" } }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 16" + "node": ">=18" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/cookie-es": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", - "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">=18" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" - }, - "node_modules/data-uri-to-buffer": { - "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==", "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 12" + "node": ">=18" } }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=6.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" }, "peerDependenciesMeta": { - "supports-color": { + "@aws-sdk/client-rds-data": { "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/drizzle-typebox": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/drizzle-typebox/-/drizzle-typebox-0.3.3.tgz", + "integrity": "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" + "license": "Apache-2.0", + "peerDependencies": { + "@sinclair/typebox": ">=0.34.8", + "drizzle-orm": ">=0.36.0" } }, - "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" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3443,6 +4658,16 @@ "dev": true, "license": "ISC" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", @@ -3525,6 +4750,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3601,6 +4836,13 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3613,6 +4855,13 @@ "node": ">=12.20.0" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3678,6 +4927,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3787,6 +5056,27 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3796,6 +5086,20 @@ "node": ">= 4" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4026,6 +5330,19 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -4041,6 +5358,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -4050,6 +5377,13 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4075,6 +5409,39 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -4123,6 +5490,16 @@ "node": ">=18" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openai": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", @@ -4322,6 +5699,34 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -4381,6 +5786,17 @@ "node": ">=12.0.0" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4391,6 +5807,22 @@ "node": ">=6" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -4429,6 +5861,21 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4439,6 +5886,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -4604,6 +6061,63 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4614,6 +6128,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4628,6 +6153,26 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strnum": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", @@ -4647,6 +6192,36 @@ "dev": true, "license": "MIT" }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4768,6 +6343,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/typebox": { "version": "1.1.38", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", @@ -4843,6 +6431,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -5511,6 +7106,13 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.20.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", diff --git a/package.json b/package.json index 2aa14cf60..784c61716 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "brunch-next", "version": "0.0.0", - "description": "Brunch \u2014 opinionated specification-workspace product over pi-coding-agent.", + "description": "Brunch — opinionated specification-workspace product over pi-coding-agent.", "private": true, "type": "module", "main": "./dist/brunch.js", @@ -42,13 +42,19 @@ "zod": "^4.4.3" }, "devDependencies": { + "@sinclair/typebox": "^0.34.14", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.7.0", + "better-sqlite3": "^12.8.0", + "drizzle-kit": "^0.31.10", + "drizzle-orm": "^0.45.2", + "drizzle-typebox": "^0.3.3", "jsdom": "^29.1.1", "oxfmt": "^0.2.0", "oxlint": "^0.18.0", @@ -59,5 +65,8 @@ }, "engines": { "node": ">=20" + }, + "allowScripts": { + "better-sqlite3@12.8.0": true } } diff --git a/src/README.md b/src/README.md new file mode 100644 index 000000000..276653644 --- /dev/null +++ b/src/README.md @@ -0,0 +1,63 @@ +# src/ — Brunch source topology + +Decision D52-L in `memory/SPEC.md` locks this layout. + +``` +src/ +├── .pi/ Pi adapter layer (TUI) +│ ├── components/ reusable TUI components +│ └── extensions/ Pi registrars: agent tools, TUI commands, enhancements +│ +├── agents/ Agent intelligence layer +│ ├── modes/ operational mode prompts and rules +│ ├── strategies/ interaction-shape prompts (propose-graph, project-graph, etc.) +│ ├── lenses/ topical-focus prompts (intent, design, oracle, etc.) +│ └── contexts/ snapshot orchestration (calls graph/ and session/) +│ +├── db/ Persistence substrate +│ Drizzle schema, migrations, connection lifecycle +│ +├── graph/ Graph domain layer +│ CommandExecutor, readers, policy, validators, +│ snapshot bucketing, change-log replay, recon-need substrate +│ +├── session/ Session domain layer +│ transcript projection, exchange extraction, +│ workspace coordination, session binding, LSN staleness +│ +├── rpc/ Brunch JSON-RPC handlers +│ protocol, method handlers, WebSocket adapter +│ +└── web/ React client (standalone build target) + routes, hooks, RPC client +``` + +## Dependency direction + +``` +.pi/extensions/ ──┐ + ├──▶ graph/ ──▶ db/ +rpc/ ────────────┤ + ├──▶ session/ +agents/ ─────────┘ + (Pi JSONL — not Brunch-owned storage) + +web/ ── standalone build, imports from rpc/ types only +``` + +Rules: +- `graph/` imports from `db/`. No other layer imports `db/` directly. +- `agents/` imports snapshot functions from `graph/` and `session/`. +- `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`. +- `web/` is a separate Vite build target. + +## Migration notes + +Some files currently at `src/` root belong in `src/session/` per this layout +(workspace-session-coordinator, session-binding, session-projection-reader, +brunch-session-envelope, session-transcript, elicitation-exchange, +structured-exchange). Move incrementally as each file is touched. + +Prompt composition currently under `src/tui-client/.pi/context/` migrates +to `src/agents/` per D52-L. The `.pi/context/` README describes the +current interim layout. diff --git a/src/agents/README.md b/src/agents/README.md new file mode 100644 index 000000000..dc9d64123 --- /dev/null +++ b/src/agents/README.md @@ -0,0 +1,97 @@ +# agents/ — Agent intelligence layer + +SPEC decisions: D25-L, D40-L, D52-L + +## Owns + +Everything that shapes what the LLM sees and does: state definitions, +prompt composition, strategy/lens content, and context snapshot orchestration. + +### Agent state hierarchy + +``` +spec.grade + grounding → elicitation I,II → commitment → export + +session.mode = elicitation | execution (future) | reconciliation (deferred) +session.agent = elicitor | planner (future) | reconciler (deferred) +session.strategy = per-agent interaction shape +session.lens = per-mode topical focus +session.sub-agents = research, explore, design, oracle, review, reconcile +``` + +### Strategy × lens (D25-L) + +Strategies describe the interaction shape. Lenses describe topical focus. +The combination maps to the prior "lens catalogue" names: + +| Strategy | Commitment path | Example lens combinations | +|--------------------------|--------------------|------------------------------------| +| `step-wise-decision-tree`| single-exchange | any lens | +| `step-wise-disambiguate` | single-exchange | any lens | +| `propose-graph` | direct commit | intent, design, oracle | +| `project-graph` | review-set | intent | + +### Context building + +Snapshot functions live in `contexts/`. They orchestrate *which* snapshots +to inject based on mode/role/strategy/lens/grade, by calling into: + +``` +agents/contexts/ + │ + ├──▶ graph/ → snapshotGraph(detail), snapshotNode(id, hops) + │ + └──▶ session/ → workspace/spec envelope +``` + +Graph snapshots support multiple detail levels (I35-L): +- **Cursory** — compact full-graph overview for orientation +- **Neighborhood** — detailed node + N-hop expansion for focused work + +## Directory layout + +``` +agents/ +├── README.md +├── state.ts mode/role/strategy/lens type defs + valid combos +├── compose.ts prompt orchestrator: reads state, picks packs, calls snapshots +├── modes/ +│ └── elicit.md elicitation mode rules, tool authority +├── strategies/ +│ ├── step-wise-decision-tree.md +│ ├── step-wise-disambiguate.md +│ ├── propose-graph.md ← graph vocabulary, category rubric, batch format +│ └── project-graph.md +├── lenses/ +│ ├── intent.md +│ ├── design.md +│ └── oracle.md +└── contexts/ + ├── graph-context.ts calls graph/ snapshot fns, formats for prompt + └── readiness-context.ts +``` + +## Does NOT own + +- Pi extension registration, tool definitions — those live in `.pi/extensions/`. +- Graph domain logic, CommandExecutor — those live in `graph/`. +- Session projection, transcript reading — those live in `session/`. + +## Imported by + +- `.pi/extensions/prompting.ts` — calls compose.ts at turn boundaries +- `.pi/extensions/operational-mode.ts` — reads state definitions + +## Migration from .pi/context/ + +The current `src/tui-client/.pi/context/` layout migrates here: + +| Current location | Target | +|-------------------------------------------|-------------------------------| +| `.pi/context/compose-brunch-prompt.ts` | `agents/compose.ts` | +| `.pi/context/prompt-packs/*.md` | `agents/modes/`, `strategies/`, `lenses/` | +| `.pi/context/builders/graph-context.ts` | `agents/contexts/graph-context.ts` | +| `.pi/context/builders/readiness-context.ts`| `agents/contexts/readiness-context.ts` | + +Move incrementally as prompt composition is refactored. diff --git a/src/agents/lenses/README.md b/src/agents/lenses/README.md new file mode 100644 index 000000000..a00b86ca2 --- /dev/null +++ b/src/agents/lenses/README.md @@ -0,0 +1,43 @@ +# lenses/ — Topical-focus prompt packs + +SPEC decisions: D25-L, D56-L + +Each lens describes a topical focus — what domain of the spec the +agent is currently exploring or proposing into. + +## Lenses + +| Lens | Plane focus | Notes | +|-----------|-------------|-----------------------------------------| +| `intent` | intent | Requirements, goals, constraints, etc. | +| `design` | design | Modules, interfaces, architecture | +| `oracle` | oracle | Checks, criteria, evidence, obligations | + +Future execute-mode lenses (`plan`, `sync`, `scope`) are deferred. + +## Topology-driven question ranking (M5 input) + +When `agent-graph-integration` lands prompt packs, each lens +should include topology-driven heuristics for what to ask next. +These heuristics read graph shape, not templates: + +| Signal | Suggested question shape | +|----------------------------------------------|---------------------------------------------------| +| `assumption` with high fanout + low confidence | "We depend on X. Want to validate it?" | +| `requirement` with no incoming `proof` edge | "How will we know this holds?" | +| `criterion` with no outgoing `proof` target | "What does this criterion check?" | +| `decision` with empty `rejected` | "What did we consider and rule out?" | +| Conflicting `boundary` edges into same target | "These constraints disagree. Which wins?" | +| `goal` with no derived requirements | "Nothing ties to this goal. What would satisfy it?" | +| `requirement` with no examples + high uncertainty | "What's a concrete case where this matters?" | + +These complement behavioral-kernel signal-phrase routing: kernels +suggest *what kind* of question; topology heuristics suggest *which +item* to ask about next. + +## Source reference + +Rich topology-driven ranking heuristics from the earlier design +are in the archived +`/brunch/docs/design/INTENT_GRAPH_SEMANTICS.md` §Topology-driven +question ranking. Treat as a prompt engineering input. diff --git a/src/agents/strategies/README.md b/src/agents/strategies/README.md new file mode 100644 index 000000000..1859e8ca6 --- /dev/null +++ b/src/agents/strategies/README.md @@ -0,0 +1,56 @@ +# strategies/ — Interaction-shape prompt packs + +SPEC decisions: D25-L, D26-L, D53-L + +Each strategy describes an interaction shape — how the agent +structures its turns, what commitment mechanism it uses, and what +the user experiences. + +## Strategies + +| Strategy | Commitment path | Notes | +|---------------------------|-----------------|------------------------------------| +| `step-wise-decision-tree` | single-exchange | Q&A one claim at a time | +| `step-wise-disambiguate` | single-exchange | contrastive examples | +| `propose-graph` | direct commit | concept → user accepts → commitGraph | +| `project-graph` | review-set | derive from existing graph | + +## Prompt pack contents + +Each `.md` file in this directory is a prompt pack injected when +the strategy is active. It should contain: + +- What the agent is doing in this strategy +- How to structure the turn +- What commitment mechanism to use +- What graph operations are available +- Category-selection rubric (for graph-writing strategies) + +## Observer classification guide (M5 input) + +When `agent-graph-integration` lands prompt packs, seed each +strategy's prompt with the observer classification rules from +the earlier `INTENT_GRAPH_SEMANTICS.md` translation table: + +| User phrase pattern | Most likely kind | +|----------------------------------|-----------------------| +| "always true that…" | `invariant` | +| "should never…" | `invariant` | +| "for example, when…" | `example` | +| "we wouldn't want…" | `example` (negative) or `constraint` | +| "we don't care about X" | `constraint` | +| "we picked Y over Z because…" | `decision` | +| "we think" / "probably" | `assumption` | +| "the system shall" / "must do" | `requirement` | +| "what outcome are we after?" | `goal` | + +The observer should **abstain** rather than guess when +classification support is weak. + +## Source reference + +Rich classification and translation tables from the earlier +design are in the archived +`/brunch/docs/design/INTENT_GRAPH_SEMANTICS.md` §Observer-prompt +classification guide and §Translation table. Treat as a prompt +engineering input, not a schema target. diff --git a/src/brunch-pi-profile.ts b/src/brunch-pi-profile.ts new file mode 100644 index 000000000..831f66951 --- /dev/null +++ b/src/brunch-pi-profile.ts @@ -0,0 +1,162 @@ +import process from "node:process" + +import { + SettingsManager, + type ExtensionFactory, +} from "@earendil-works/pi-coding-agent" + +export const BRUNCH_SETTINGS_POLICY = { + quietStartup: true, + packages: [], + extensions: [], + skills: [], + prompts: [], + themes: [], + enableSkillCommands: false, + doubleEscapeAction: "none", + compaction: { + enabled: true, + reserveTokens: 16384, + keepRecentTokens: 20000, + }, + branchSummary: { + reserveTokens: 16384, + skipPrompt: false, + }, + retry: { + enabled: true, + maxRetries: 3, + baseDelayMs: 2000, + provider: { + maxRetryDelayMs: 60000, + }, + }, + terminal: { + showImages: true, + imageWidthCells: 60, + clearOnShrink: false, + showTerminalProgress: false, + }, + images: { + autoResize: true, + blockImages: false, + }, + transport: "auto", + collapseChangelog: false, + enableInstallTelemetry: false, + showHardwareCursor: false, + editorPaddingX: 0, + autocompleteMaxVisible: 5, + markdown: { + codeBlockIndent: " ", + }, + warnings: {}, +} satisfies Parameters[0] + +export const BRUNCH_SETTINGS_AUDITED_GETTERS = [ + "getGlobalSettings", + "getProjectSettings", + "getLastChangelogVersion", + "getSessionDir", + "getDefaultProvider", + "getDefaultModel", + "getSteeringMode", + "getFollowUpMode", + "getTheme", + "getDefaultThinkingLevel", + "getTransport", + "getCompactionEnabled", + "getCompactionReserveTokens", + "getCompactionKeepRecentTokens", + "getCompactionSettings", + "getBranchSummarySettings", + "getBranchSummarySkipPrompt", + "getRetryEnabled", + "getRetrySettings", + "getProviderRetrySettings", + "getHideThinkingBlock", + "getShellPath", + "getQuietStartup", + "getShellCommandPrefix", + "getNpmCommand", + "getCollapseChangelog", + "getEnableInstallTelemetry", + "getPackages", + "getExtensionPaths", + "getSkillPaths", + "getPromptTemplatePaths", + "getThemePaths", + "getEnableSkillCommands", + "getThinkingBudgets", + "getShowImages", + "getImageWidthCells", + "getClearOnShrink", + "getShowTerminalProgress", + "getImageAutoResize", + "getBlockImages", + "getEnabledModels", + "getDoubleEscapeAction", + "getTreeFilterMode", + "getShowHardwareCursor", + "getEditorPaddingX", + "getAutocompleteMaxVisible", + "getCodeBlockIndent", + "getWarnings", +] as const + +export interface BrunchPiProfileOptions { + cwd: string + agentDir: string + extensionFactories: ExtensionFactory[] +} + +export interface BrunchPiProfile { + settingsManager: SettingsManager + resourceLoaderOptions: BrunchResourceLoaderOptions +} + +export interface BrunchResourceLoaderOptions { + noContextFiles: true + noExtensions: true + noPromptTemplates: true + noSkills: true + noThemes: true + extensionFactories: ExtensionFactory[] +} + +export function createBrunchPiProfile({ + cwd, + agentDir, + extensionFactories, +}: BrunchPiProfileOptions): BrunchPiProfile { + return { + settingsManager: createBrunchSettingsManager(cwd, agentDir), + resourceLoaderOptions: brunchResourceLoaderOptions(extensionFactories), + } +} + +export function brunchResourceLoaderOptions( + extensionFactories: ExtensionFactory[], +): BrunchResourceLoaderOptions { + return { + noContextFiles: true, + noExtensions: true, + noPromptTemplates: true, + noSkills: true, + noThemes: true, + extensionFactories, + } +} + +export function applyBrunchOfflineDefault( + env: { PI_OFFLINE?: string } = process.env, +): void { + env.PI_OFFLINE ??= "1" +} + +export function createBrunchSettingsManager( + _cwd: string, + _agentDir: string, +): SettingsManager { + return SettingsManager.inMemory(BRUNCH_SETTINGS_POLICY) +} diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index efcbf1acc..ae660c1f9 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -1,5 +1,5 @@ import { userMessage } from "./test-helpers.js" -import { mkdtemp, readFile } from "node:fs/promises" +import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" @@ -14,11 +14,14 @@ import { } from "@earendil-works/pi-coding-agent" import { + BRUNCH_SETTINGS_AUDITED_GETTERS, + BRUNCH_SETTINGS_POLICY, applyBrunchOfflineDefault, brunchResourceLoaderOptions, createBrunchSettingsManager, runBrunchTui, } from "./brunch-tui.js" +import { createBrunchPiProfile } from "./brunch-pi-profile.js" import { BRUNCH_WORKSPACE_COMMAND, BRUNCH_WORKSPACE_SHORTCUT, @@ -762,8 +765,227 @@ describe("Brunch TUI boot", () => { }) expect(env.PI_OFFLINE).toBe("1") }) + + it("ignores hostile ambient Pi settings for behavior-shaping profile policy", async () => { + const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) + const agentDir = join(cwd, "home-pi") + await writeHostilePiSettings(cwd, agentDir) + + const settingsManager = createBrunchSettingsManager(cwd, agentDir) + + expect(settingsManager.getShellPath()).toBeUndefined() + expect(settingsManager.getShellCommandPrefix()).toBeUndefined() + expect(settingsManager.getNpmCommand()).toBeUndefined() + expect(settingsManager.getPackages()).toEqual([]) + expect(settingsManager.getExtensionPaths()).toEqual([]) + expect(settingsManager.getSkillPaths()).toEqual([]) + expect(settingsManager.getPromptTemplatePaths()).toEqual([]) + expect(settingsManager.getThemePaths()).toEqual([]) + expect(settingsManager.getEnableSkillCommands()).toBe(false) + expect(settingsManager.getDoubleEscapeAction()).toBe("none") + expect(settingsManager.getCompactionSettings()).toEqual({ + enabled: true, + reserveTokens: 16384, + keepRecentTokens: 20000, + }) + expect(settingsManager.getRetrySettings()).toEqual({ + enabled: true, + maxRetries: 3, + baseDelayMs: 2000, + }) + expect(settingsManager.getProviderRetrySettings()).toEqual({ + timeoutMs: undefined, + maxRetries: undefined, + maxRetryDelayMs: 60000, + }) + expect(settingsManager.getShowImages()).toBe(true) + expect(settingsManager.getImageWidthCells()).toBe(60) + expect(settingsManager.getClearOnShrink()).toBe(false) + expect(settingsManager.getShowTerminalProgress()).toBe(false) + expect(settingsManager.getImageAutoResize()).toBe(true) + expect(settingsManager.getBlockImages()).toBe(false) + expect(settingsManager.getTransport()).toBe("auto") + expect(settingsManager.getTheme()).toBeUndefined() + expect(settingsManager.getLastChangelogVersion()).toBeUndefined() + expect(settingsManager.getCollapseChangelog()).toBe(false) + expect(settingsManager.getEnableInstallTelemetry()).toBe(false) + expect(settingsManager.getShowHardwareCursor()).toBe(false) + expect(settingsManager.getEditorPaddingX()).toBe(0) + expect(settingsManager.getAutocompleteMaxVisible()).toBe(5) + }) + + it("keeps sealed Brunch settings after Pi settings reload", async () => { + const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) + const agentDir = join(cwd, "home-pi") + await writeHostilePiSettings(cwd, agentDir) + const settingsManager = createBrunchSettingsManager(cwd, agentDir) + + await settingsManager.reload() + + expect(settingsManager.getQuietStartup()).toBe(true) + expect(settingsManager.getPackages()).toEqual([]) + expect(settingsManager.getExtensionPaths()).toEqual([]) + expect(settingsManager.getSkillPaths()).toEqual([]) + expect(settingsManager.getPromptTemplatePaths()).toEqual([]) + expect(settingsManager.getThemePaths()).toEqual([]) + expect(settingsManager.getEnableSkillCommands()).toBe(false) + expect(settingsManager.getDoubleEscapeAction()).toBe("none") + expect(settingsManager.getShellPath()).toBeUndefined() + expect(settingsManager.getNpmCommand()).toBeUndefined() + }) + + it("keeps ambient resource suppression and explicit product extensions behind one profile boundary", async () => { + const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) + const extension = () => {} + const profile = createBrunchPiProfile({ + cwd, + agentDir: cwd, + extensionFactories: [extension], + }) + + expect(profile.settingsManager.getQuietStartup()).toBe(true) + expect(profile.resourceLoaderOptions).toEqual({ + noContextFiles: true, + noExtensions: true, + noPromptTemplates: true, + noSkills: true, + noThemes: true, + extensionFactories: [extension], + }) + }) + + it("keeps Pi settings/resource policy out of the TUI launcher", async () => { + const launcherSource = await readFile( + join(import.meta.dirname, "brunch-tui.ts"), + "utf8", + ) + const profileSource = await readFile( + join(import.meta.dirname, "brunch-pi-profile.ts"), + "utf8", + ) + + expect(launcherSource).toContain("createBrunchPiProfile") + expect(launcherSource).not.toContain("SettingsManager.create") + expect(launcherSource).not.toContain("noContextFiles") + expect(profileSource).toContain("SettingsManager.inMemory") + expect(profileSource).toContain("noContextFiles: true") + }) + + it("keeps the Brunch settings override and audit list in the profile boundary", async () => { + const launcherSource = await readFile( + join(import.meta.dirname, "brunch-tui.ts"), + "utf8", + ) + const profileSource = await readFile( + join(import.meta.dirname, "brunch-pi-profile.ts"), + "utf8", + ) + const settingsManagerTypes = await readFile( + join( + import.meta.dirname, + "..", + "node_modules", + "@earendil-works", + "pi-coding-agent", + "dist", + "core", + "settings-manager.d.ts", + ), + "utf8", + ) + const getterNames = Array.from( + settingsManagerTypes.matchAll(/\n (get[A-Z][A-Za-z0-9]+)\(/g), + (match) => match[1]!, + ) + + expect(BRUNCH_SETTINGS_POLICY).toMatchObject({ + quietStartup: true, + packages: [], + extensions: [], + skills: [], + prompts: [], + themes: [], + enableSkillCommands: false, + doubleEscapeAction: "none", + }) + expect(getterNames.sort()).toEqual( + [...BRUNCH_SETTINGS_AUDITED_GETTERS].sort(), + ) + expect(launcherSource).not.toContain("SettingsManager.inMemory") + expect(profileSource).toContain("BRUNCH_SETTINGS_POLICY") + expect(profileSource).toContain("SettingsManager.inMemory") + }) }) +async function writeHostilePiSettings( + cwd: string, + agentDir: string, +): Promise { + const hostileSettings = { + lastChangelogVersion: "999.0.0-hostile", + defaultProvider: "hostile-provider", + defaultModel: "hostile-model", + transport: "websocket", + theme: "hostile-theme", + compaction: { + enabled: false, + reserveTokens: 1, + keepRecentTokens: 2, + }, + branchSummary: { + reserveTokens: 3, + skipPrompt: true, + }, + retry: { + enabled: false, + maxRetries: 99, + baseDelayMs: 1, + provider: { + timeoutMs: 1, + maxRetries: 99, + maxRetryDelayMs: 2, + }, + }, + shellPath: "/tmp/hostile-shell", + quietStartup: false, + shellCommandPrefix: "hostile-prefix", + npmCommand: ["hostile-npm"], + collapseChangelog: true, + enableInstallTelemetry: true, + packages: ["hostile-package"], + extensions: ["hostile-extension"], + skills: ["hostile-skill"], + prompts: ["hostile-prompt"], + themes: ["hostile-theme-path"], + enableSkillCommands: true, + terminal: { + showImages: false, + imageWidthCells: 1, + clearOnShrink: true, + showTerminalProgress: true, + }, + images: { + autoResize: false, + blockImages: true, + }, + doubleEscapeAction: "tree", + showHardwareCursor: true, + editorPaddingX: 3, + autocompleteMaxVisible: 20, + } + + await mkdir(agentDir, { recursive: true }) + await mkdir(join(cwd, ".pi"), { recursive: true }) + await writeFile( + join(agentDir, "settings.json"), + JSON.stringify(hostileSettings, null, 2), + ) + await writeFile( + join(cwd, ".pi", "settings.json"), + JSON.stringify(hostileSettings, null, 2), + ) +} + function readyWorkspace( cwd: string, sessionId: string, diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index e66b71efb..21f3e21eb 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -6,9 +6,7 @@ import { createAgentSessionServices, getAgentDir, InteractiveMode, - SettingsManager, type CreateAgentSessionRuntimeFactory, - type ExtensionFactory, } from "@earendil-works/pi-coding-agent" import { @@ -24,6 +22,20 @@ import { createBrunchPiExtensionShell, } from "./tui-client/pi-extension-shell.js" import { runWorkspaceDialogPreflight } from "./tui-client/.pi/components/workspace-dialog.js" +import { + applyBrunchOfflineDefault, + brunchResourceLoaderOptions, + createBrunchPiProfile, + createBrunchSettingsManager, +} from "./brunch-pi-profile.js" +export { + BRUNCH_SETTINGS_AUDITED_GETTERS, + BRUNCH_SETTINGS_POLICY, + applyBrunchOfflineDefault, + brunchResourceLoaderOptions, + createBrunchPiProfile, + createBrunchSettingsManager, +} from "./brunch-pi-profile.js" export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, chromeStateForWorkspace, @@ -103,12 +115,10 @@ async function launchPiInteractive({ agentDir: runtimeAgentDir, sessionManager, }) => { - const settingsManager = createBrunchSettingsManager(cwd, runtimeAgentDir) - const services = await createAgentSessionServices({ + const profile = createBrunchPiProfile({ cwd, agentDir: runtimeAgentDir, - settingsManager, - resourceLoaderOptions: brunchResourceLoaderOptions([ + extensionFactories: [ createBrunchPiExtensionShell( chromeStateForWorkspace(workspace), async (sessionManager) => { @@ -118,7 +128,13 @@ async function launchPiInteractive({ }, { coordinator }, ), - ]), + ], + }) + const services = await createAgentSessionServices({ + cwd, + agentDir: runtimeAgentDir, + settingsManager: profile.settingsManager, + resourceLoaderOptions: profile.resourceLoaderOptions, }) const created = await createAgentSessionFromServices({ services, @@ -140,31 +156,3 @@ async function launchPiInteractive({ applyBrunchOfflineDefault() await new InteractiveMode(runtime).run() } - -export function brunchResourceLoaderOptions( - extensionFactories: ExtensionFactory[], -) { - return { - noContextFiles: true, - noExtensions: true, - noPromptTemplates: true, - noSkills: true, - noThemes: true, - extensionFactories, - } -} - -export function applyBrunchOfflineDefault( - env: { PI_OFFLINE?: string } = process.env, -): void { - env.PI_OFFLINE ??= "1" -} - -export function createBrunchSettingsManager( - cwd: string, - agentDir: string, -): SettingsManager { - const settingsManager = SettingsManager.create(cwd, agentDir) - settingsManager.getQuietStartup = () => true - return settingsManager -} diff --git a/src/db/README.md b/src/db/README.md new file mode 100644 index 000000000..b45565ce9 --- /dev/null +++ b/src/db/README.md @@ -0,0 +1,57 @@ +# db/ — Persistence substrate + +SPEC decisions: D16-L, D41-L, D52-L + +## Owns + +- **Drizzle table definitions** (`schema.ts`) — nodes, edges, change_log, + graph_clock, reconciliation_need. Canonical column-level source of truth + for persisted shapes. Exports shared enum `const` arrays (`INTENT_KINDS`, + `EDGE_CATEGORIES`, etc.) reused by `graph/` domain types and Pi tool + parameter schemas. + +- **Row schema derivation** (`row-schemas.ts`) — runtime insert/select + schemas derived from Drizzle tables via `drizzle-typebox`. Do not + hand-author parallel row schemas alongside table definitions. + +- **Connection lifecycle** (`connection.ts`) — `better-sqlite3` connection + creation, WAL mode, pragmas, migration runner. + +- **Migrations** — Drizzle-managed schema migrations for `.brunch/data.db`. + Wired when the first `drizzle-kit generate` run lands. + +## Does NOT own + +- Domain logic, validation, policy, CommandExecutor, readers, change-log + replay — all of that lives in `graph/`. +- Query construction beyond simple helpers — domain queries live in `graph/`. + +## Imported by + +- `graph/` — the only layer that imports `db/` directly. + No other layer should import from this directory. + +## Enum flow + +`db/schema.ts` owns the single `const` arrays (`INTENT_KINDS`, +`EDGE_CATEGORIES`, `NODE_BASES`, etc.). Other layers derive from them: + +``` +db/schema.ts const arrays + Drizzle tables + │ (single source of truth) + ├──► db/row-schemas.ts drizzle-typebox insert/select + │ (@sinclair/typebox 0.34) + ├──► graph/schema/nodes.ts type IntentKind = (typeof INTENT_KINDS)[number] + │ graph/schema/edges.ts type EdgeCategory = (typeof EDGE_CATEGORIES)[number] + │ + └──► Pi tool parameter schemas Type.Union(INTENT_KINDS.map(Type.Literal)) + (typebox v1.x — Pi's package) +``` + +Do not redeclare enum literals in `graph/` or tool definitions. +Import the `const` array from `db/schema.ts` and derive. + +## Stack (settled by A20-L spike) + +`drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` ++ `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14` diff --git a/src/db/connection.ts b/src/db/connection.ts new file mode 100644 index 000000000..2e9022879 --- /dev/null +++ b/src/db/connection.ts @@ -0,0 +1,94 @@ +/** + * better-sqlite3 connection lifecycle. + * + * SPEC decisions: D16-L (settled by A20-L spike) + * Stack: drizzle-orm@0.45.2 + better-sqlite3@12.8.0 + */ + +import Database from "better-sqlite3" +import { drizzle } from "drizzle-orm/better-sqlite3" + +import * as schema from "./schema.js" + +export type BrunchDb = ReturnType> + +/** + * Create a Brunch database connection with schema initialized. + * + * Creates all tables if they don't exist and seeds the graph_clock + * with lsn=0. For tests, pass `":memory:"` for an in-memory database. + * + * When real migrations are needed (existing data to transform), + * replace `initSchema` with `drizzle-kit`-managed migrations. + */ +export function createDb(path: string): BrunchDb { + const sqlite = new Database(path) + sqlite.pragma("journal_mode = WAL") + sqlite.pragma("foreign_keys = ON") + initSchema(sqlite) + return drizzle(sqlite, { schema }) +} + +/** + * Push schema DDL and seed initial data. + * + * This replaces drizzle-kit migrations for the initial M4 slice. + * Pre-release posture: no existing data to preserve, so CREATE IF + * NOT EXISTS is sufficient. Add migration files when schema evolution + * needs data transformation. + */ +function initSchema(sqlite: Database.Database): void { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + plane TEXT NOT NULL, + kind TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT, + basis TEXT NOT NULL DEFAULT 'explicit', + source TEXT, + detail TEXT, + created_at_lsn INTEGER NOT NULL, + updated_at_lsn INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category TEXT NOT NULL, + source_id INTEGER NOT NULL REFERENCES nodes(id), + target_id INTEGER NOT NULL REFERENCES nodes(id), + stance TEXT, + basis TEXT NOT NULL DEFAULT 'explicit', + rationale TEXT, + created_at_lsn INTEGER NOT NULL, + updated_at_lsn INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS graph_clock ( + id INTEGER PRIMARY KEY, + lsn INTEGER NOT NULL DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS change_log ( + lsn INTEGER PRIMARY KEY, + operation TEXT NOT NULL, + payload TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS reconciliation_need ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target_kind TEXT NOT NULL, + target_edge_id INTEGER REFERENCES edges(id), + target_a_id INTEGER REFERENCES nodes(id), + target_b_id INTEGER REFERENCES nodes(id), + kind TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'open', + reason TEXT, + created_at_lsn INTEGER NOT NULL, + resolved_at_lsn INTEGER + ); + + INSERT OR IGNORE INTO graph_clock (id, lsn) VALUES (1, 0); + `) +} diff --git a/src/db/row-schemas.ts b/src/db/row-schemas.ts new file mode 100644 index 000000000..ae5bd15a0 --- /dev/null +++ b/src/db/row-schemas.ts @@ -0,0 +1,40 @@ +/** + * Runtime row schemas derived from Drizzle table definitions. + * + * SPEC decisions: D16-L, D41-L (settled by A20-L spike) + * Stack: drizzle-typebox@0.3.3 + @sinclair/typebox@0.34.14 + * + * Do not hand-author parallel row schemas. These are the single + * derived source for insert/select validation inside db/ and graph/. + */ + +import { createInsertSchema, createSelectSchema } from "drizzle-typebox" + +import { + changeLog, + edges, + graphClock, + nodes, + reconciliationNeed, +} from "./schema.js" + +// --- Node schemas --- +export const insertNodeSchema = createInsertSchema(nodes) +export const selectNodeSchema = createSelectSchema(nodes) + +// --- Edge schemas --- +export const insertEdgeSchema = createInsertSchema(edges) +export const selectEdgeSchema = createSelectSchema(edges) + +// --- Change log schemas --- +export const insertChangeLogSchema = createInsertSchema(changeLog) +export const selectChangeLogSchema = createSelectSchema(changeLog) + +// --- Graph clock schemas --- +export const insertGraphClockSchema = createInsertSchema(graphClock) + +// --- Reconciliation need schemas --- +export const insertReconciliationNeedSchema = + createInsertSchema(reconciliationNeed) +export const selectReconciliationNeedSchema = + createSelectSchema(reconciliationNeed) diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 000000000..16622d4e7 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,118 @@ +/** + * Drizzle table definitions — canonical column-level source of truth. + * + * SPEC decisions: D16-L, D51-L, D54-L, D56-L + * Canonical reference: docs/design/GRAPH_MODEL.md + * + * Enum const arrays are exported for reuse by graph/ domain types + * and by Pi tool parameter schemas (via typebox v1.x). + */ + +import { sql } from "drizzle-orm" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" + +// --------------------------------------------------------------------------- +// Shared enum arrays — the single source for text enum columns, +// graph/ domain types, and Pi tool parameter schemas. +// --------------------------------------------------------------------------- + +export const INTENT_KINDS = [ + "goal", + "thesis", + "term", + "context", + "requirement", + "assumption", + "constraint", + "invariant", + "decision", + "criterion", + "example", +] as const + +export const ORACLE_KINDS = [ + "check", + "validation_method", + "evidence", + "obligation", +] as const + +export const DESIGN_KINDS = ["module", "interface"] as const + +export const PLAN_KINDS = ["milestone", "frontier", "slice"] as const + +export const NODE_BASES = ["explicit", "accepted_review_set"] as const + +export const EDGE_CATEGORIES = [ + "dependency", + "proof", + "support", + "realization", + "boundary", + "composition", + "association", + "supersession", +] as const + +export const EDGE_STANCES = ["for", "against"] as const + +// --------------------------------------------------------------------------- +// Tables +// --------------------------------------------------------------------------- + +export const nodes = sqliteTable("nodes", { + id: integer().primaryKey({ autoIncrement: true }), + plane: text({ enum: ["intent", "oracle", "design", "plan"] }).notNull(), + kind: text().notNull(), // validated at domain layer against plane-specific enum + title: text().notNull(), + body: text(), + basis: text({ enum: NODE_BASES }).notNull().default("explicit"), + source: text(), + detail: text(), // JSON column: decision → {chosen_option, rejected, rationale}, term → {definition, aliases?} + created_at_lsn: integer().notNull(), + updated_at_lsn: integer().notNull(), +}) + +export const edges = sqliteTable("edges", { + id: integer().primaryKey({ autoIncrement: true }), + category: text({ enum: EDGE_CATEGORIES }).notNull(), + source_id: integer() + .notNull() + .references(() => nodes.id), + target_id: integer() + .notNull() + .references(() => nodes.id), + stance: text({ enum: EDGE_STANCES }), + basis: text({ enum: NODE_BASES }).notNull().default("explicit"), + rationale: text(), + created_at_lsn: integer().notNull(), + updated_at_lsn: integer().notNull(), +}) + +export const graphClock = sqliteTable("graph_clock", { + id: integer().primaryKey(), // always row 1 + lsn: integer().notNull().default(0), +}) + +export const changeLog = sqliteTable("change_log", { + lsn: integer().primaryKey(), + operation: text().notNull(), + payload: text().notNull(), // JSON summary of the mutation + created_at: text().notNull().default(sql`(datetime('now'))`), +}) + +export const reconciliationNeed = sqliteTable("reconciliation_need", { + id: integer().primaryKey({ autoIncrement: true }), + // target is {kind:'edge', edgeId} or {kind:'node_pair', aId, bId} + target_kind: text({ enum: ["edge", "node_pair"] }).notNull(), + target_edge_id: integer().references(() => edges.id), + target_a_id: integer().references(() => nodes.id), + target_b_id: integer().references(() => nodes.id), + kind: text().notNull(), // substantive taxonomy deferred per A8-L + status: text({ enum: ["open", "resolved"] }) + .notNull() + .default("open"), + reason: text(), + created_at_lsn: integer().notNull(), + resolved_at_lsn: integer(), +}) diff --git a/src/graph/README.md b/src/graph/README.md new file mode 100644 index 000000000..82851e931 --- /dev/null +++ b/src/graph/README.md @@ -0,0 +1,73 @@ +# graph/ — Graph domain layer + +Canonical reference: `docs/design/GRAPH_MODEL.md` +SPEC decisions: D4-L, D20-L, D51-L, D52-L, D53-L + +## Owns + +- **CommandExecutor** — the single mutation boundary for all graph writes. + Hides validation, LSN allocation, change-log append, transaction mechanics. + Returns structured results: `ok`, `needs_human`, `policy_blocked`, + `version_conflict`, `structural_illegal`. + +- **commitGraph** (D53-L) — atomic batch mutation accepting `{ nodes, edges }` + with intra-batch refs (`"n1"`) and existing-node refs. One tool call, + one LSN, all-or-nothing (I34-L). The load-bearing tool for propose-graph. + +- **Readers / snapshot functions** — graph queries at multiple detail levels: + cursory full-graph overview, node-neighborhood with configurable hops (I35-L). + Called by `agents/contexts/` for prompt injection. + +- **Policy** — per-category edge policy (cascade, recon-need triggers, + criteria-help signals, projection effects). + +- **Validators** — structural legality checks: closed edge-category set, + stance rules, supersession acyclicity, framing matrix, intra-batch + reference resolution. + +- **Change-log replay** — ordered mutation history keyed by LSN. + +- **Reconciliation-need substrate** — separate from graph edges; + target is `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}`. + +## Imports from + +- `db/` — Drizzle table definitions, connection handle. + This is the only layer that touches `db/` directly. + +## Imported by + +- `.pi/extensions/graph/` — Pi tool adapters call CommandExecutor +- `rpc/` — graph.* RPC handlers call readers and CommandExecutor +- `agents/contexts/` — snapshot functions for prompt context + +## Current state (Phase 1 stubs) + +``` +graph/ +├── atoms.ts NodeId, EdgeId, Lsn type aliases +├── index.ts public re-exports +├── schema/ +│ ├── edges.ts GraphEdge, EdgeCategory, EdgeStance, EdgeBasis +│ └── reconciliation-need.ts ReconciliationNeed types +└── policy/ + └── category-policy.ts CATEGORY_POLICY table +``` + +## Target state (after M4) + +``` +graph/ +├── atoms.ts +├── index.ts +├── command-executor.ts CommandExecutor + result types +├── commit-graph.ts batch validation + intra-batch ref resolution +├── readers.ts snapshot queries (cursory, neighborhood) +├── change-log.ts replay, changesSince +├── schema/ +│ ├── edges.ts +│ ├── nodes.ts Phase 2 — per-plane node kinds +│ └── reconciliation-need.ts +└── policy/ + └── category-policy.ts +``` diff --git a/src/graph/architecture.test.ts b/src/graph/architecture.test.ts new file mode 100644 index 000000000..5aab75c37 --- /dev/null +++ b/src/graph/architecture.test.ts @@ -0,0 +1,30 @@ +/** + * I26-L architectural boundary test. + * + * Enforces: only `graph/` imports from `db/` directly. + * No other `src/` layer may import `db/` modules. + * + * SPEC: D52-L, I26-L + */ + +import { execSync } from "node:child_process" +import { describe, expect, it } from "vitest" + +describe("I26-L architectural boundary", () => { + it("no src/ module outside graph/ imports from db/", () => { + // Find all .ts files importing from db/ (excluding graph/, db/ itself, + // and test files within graph/) + const result = execSync( + `rg --files-with-matches "from ['\\"]\\.\\./db/|from ['\\"]\\.\\./\\.\\./db/|from ['\\"]\\./db/" src/ --glob '*.ts' --glob '!*.test.*' || true`, + { cwd: process.cwd(), encoding: "utf-8" }, + ) + + const importingFiles = result + .trim() + .split("\n") + .filter(Boolean) + .filter((f) => !f.startsWith("src/graph/") && !f.startsWith("src/db/")) + + expect(importingFiles).toEqual([]) + }) +}) diff --git a/src/graph/atoms.ts b/src/graph/atoms.ts new file mode 100644 index 000000000..a933d8c12 --- /dev/null +++ b/src/graph/atoms.ts @@ -0,0 +1,18 @@ +/** + * Graph atoms — id and clock primitives shared across the graph layer. + * + * Canonical reference: docs/design/GRAPH_MODEL.md §"Atoms" + * + * Phase 1 lock-and-materialize: type definitions only. + * Persistence (Drizzle + better-sqlite3 tables, LSN allocation, change_log) + * lands with the M4 A20-L spike slice. + */ + +/** Stable id for a graph node (SQLite auto-increment integer). */ +export type NodeId = number + +/** Stable id for a graph edge (SQLite auto-increment integer). */ +export type EdgeId = number + +/** Monotonic logical sequence number; one per CommandExecutor commit. */ +export type Lsn = number diff --git a/src/graph/command-executor.test.ts b/src/graph/command-executor.test.ts new file mode 100644 index 000000000..29b9a7985 --- /dev/null +++ b/src/graph/command-executor.test.ts @@ -0,0 +1,922 @@ +/** + * CommandExecutor tests — acceptance criteria for the M4 skeleton slice. + * + * SPEC: D4-L, D20-L, D16-L, D52-L + * Scope card: CommandExecutor skeleton with single-node proof-of-life + */ + +import { describe, expect, it, beforeEach } from "vitest" + +import { createDb, type BrunchDb } from "../db/connection.js" +import { graphClock, changeLog, edges, nodes } from "../db/schema.js" +import { CommandExecutor } from "./command-executor.js" +import type { CommitGraphInput } from "./command-executor.js" + +function createTestDb(): BrunchDb { + return createDb(":memory:") +} + +describe("CommandExecutor", () => { + let db: BrunchDb + let executor: CommandExecutor + + beforeEach(() => { + db = createTestDb() + executor = new CommandExecutor(db) + }) + + // --- graph_clock initialization --- + + it("initializes graph_clock with lsn=0", () => { + const rows = db.select().from(graphClock).all() + expect(rows).toHaveLength(1) + expect(rows[0]!.lsn).toBe(0) + }) + + // --- createNode: success path --- + + it("creates a valid intent node and returns success with nodeId and lsn", () => { + const result = executor.createNode({ + plane: "intent", + kind: "requirement", + title: "System must be offline-capable", + body: "Works without network connectivity", + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + expect(result.nodeId).toBeTypeOf("number") + expect(result.lsn).toBe(1) + }) + + it("defaults basis to 'explicit' when omitted", () => { + executor.createNode({ + plane: "intent", + kind: "goal", + title: "Some goal", + }) + + const row = db.select().from(nodes).all()[0] + expect(row!.basis).toBe("explicit") + }) + + it("stores optional body and source fields", () => { + executor.createNode({ + plane: "intent", + kind: "context", + title: "Target market", + body: "Enterprise B2B SaaS", + source: "stakeholder", + }) + + const row = db.select().from(nodes).all()[0] + expect(row!.body).toBe("Enterprise B2B SaaS") + expect(row!.source).toBe("stakeholder") + }) + + it("creates a decision node with required detail", () => { + const result = executor.createNode({ + plane: "intent", + kind: "decision", + title: "Use SQLite for persistence", + detail: { + chosen_option: "SQLite via better-sqlite3", + rejected: ["PostgreSQL", "In-memory only"], + rationale: "Local-first single-process, no server needed", + }, + }) + + expect(result.status).toBe("success") + const row = db.select().from(nodes).all()[0] + expect(row!.detail).not.toBeNull() + const detail = JSON.parse(row!.detail!) + expect(detail.chosen_option).toBe("SQLite via better-sqlite3") + expect(detail.rejected).toEqual(["PostgreSQL", "In-memory only"]) + }) + + it("creates a term node with required detail", () => { + const result = executor.createNode({ + plane: "intent", + kind: "term", + title: "Reconciliation Need", + detail: { + definition: "A record of an open impasse over graph state", + aliases: ["recon need", "impasse"], + }, + }) + + expect(result.status).toBe("success") + const row = db.select().from(nodes).all()[0] + const detail = JSON.parse(row!.detail!) + expect(detail.definition).toBe( + "A record of an open impasse over graph state", + ) + expect(detail.aliases).toEqual(["recon need", "impasse"]) + }) + + // --- createNode: structural_illegal rejections --- + + it("rejects invalid kind for plane", () => { + const result = executor.createNode({ + plane: "intent", + kind: "check", // oracle-plane kind, not intent + title: "Wrong plane", + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field === "kind")).toBe(true) + }) + + it("rejects decision without detail", () => { + const result = executor.createNode({ + plane: "intent", + kind: "decision", + title: "Some decision", + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field === "detail")).toBe(true) + }) + + it("rejects term without detail", () => { + const result = executor.createNode({ + plane: "intent", + kind: "term", + title: "Some term", + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field === "detail")).toBe(true) + }) + + it("rejects non-decision/term node with detail present", () => { + const result = executor.createNode({ + plane: "intent", + kind: "requirement", + title: "Some requirement", + detail: { definition: "should not be here" }, + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field === "detail")).toBe(true) + }) + + it("rejects decision with empty rejected array", () => { + const result = executor.createNode({ + plane: "intent", + kind: "decision", + title: "Bad decision", + detail: { + chosen_option: "A", + rejected: [], + rationale: "because", + }, + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field === "detail.rejected")).toBe( + true, + ) + }) + + it("rejects decision detail with unknown fields", () => { + const result = executor.createNode({ + plane: "intent", + kind: "decision", + title: "Leaky decision", + detail: { + chosen_option: "A", + rejected: ["B"], + rationale: "because", + extra_field: "should not be here", + }, + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect( + result.diagnostics.some((d) => d.field === "detail.extra_field"), + ).toBe(true) + }) + + // --- LSN / graph_clock --- + + it("increments graph_clock atomically per command", () => { + executor.createNode({ + plane: "intent", + kind: "goal", + title: "First", + }) + executor.createNode({ + plane: "intent", + kind: "goal", + title: "Second", + }) + + const [clock] = db.select().from(graphClock).all() + expect(clock!.lsn).toBe(2) + }) + + it("assigns matching created_at_lsn and updated_at_lsn on new nodes", () => { + const result = executor.createNode({ + plane: "intent", + kind: "assumption", + title: "Pi exposes enough seams", + }) + + if (result.status !== "success") throw new Error("unreachable") + const row = db.select().from(nodes).all()[0] + expect(row!.created_at_lsn).toBe(result.lsn) + expect(row!.updated_at_lsn).toBe(result.lsn) + }) + + it("LSN is strictly monotonic across multiple creates", () => { + const lsns: number[] = [] + for (let i = 0; i < 10; i++) { + const result = executor.createNode({ + plane: "intent", + kind: "context", + title: `Context ${i}`, + }) + if (result.status !== "success") throw new Error("unreachable") + lsns.push(result.lsn) + } + + for (let i = 1; i < lsns.length; i++) { + expect(lsns[i]).toBe(lsns[i - 1]! + 1) + } + }) + + // --- change_log --- + + it("appends exactly one change_log entry per successful command", () => { + executor.createNode({ + plane: "intent", + kind: "requirement", + title: "Must persist", + }) + + const logs = db.select().from(changeLog).all() + expect(logs).toHaveLength(1) + expect(logs[0]!.operation).toBe("create_node") + }) + + it("change_log payload contains nodeId, plane, and kind", () => { + const result = executor.createNode({ + plane: "intent", + kind: "invariant", + title: "LSN monotonicity", + }) + + if (result.status !== "success") throw new Error("unreachable") + const [log] = db.select().from(changeLog).all() + const payload = JSON.parse(log!.payload) + expect(payload.nodeId).toBe(result.nodeId) + expect(payload.plane).toBe("intent") + expect(payload.kind).toBe("invariant") + }) + + it("change_log.lsn matches the command's allocated LSN", () => { + const result = executor.createNode({ + plane: "intent", + kind: "goal", + title: "Test", + }) + + if (result.status !== "success") throw new Error("unreachable") + const [log] = db.select().from(changeLog).all() + expect(log!.lsn).toBe(result.lsn) + }) + + // --- Transaction integrity --- + + it("writes nothing on validation failure (no LSN bump, no change_log)", () => { + executor.createNode({ + plane: "intent", + kind: "check", // invalid kind for intent plane + title: "Should fail", + }) + + const [clock] = db.select().from(graphClock).all() + expect(clock!.lsn).toBe(0) + expect(db.select().from(nodes).all()).toHaveLength(0) + expect(db.select().from(changeLog).all()).toHaveLength(0) + }) + + // --- Oracle/design/plan plane nodes --- + + it("creates oracle-plane nodes", () => { + const result = executor.createNode({ + plane: "oracle", + kind: "check", + title: "Verify LSN monotonicity", + }) + + expect(result.status).toBe("success") + }) + + it("creates design-plane nodes", () => { + const result = executor.createNode({ + plane: "design", + kind: "module", + title: "CommandExecutor", + }) + + expect(result.status).toBe("success") + }) + + it("creates plan-plane nodes", () => { + const result = executor.createNode({ + plane: "plan", + kind: "slice", + title: "M4 skeleton", + }) + + expect(result.status).toBe("success") + }) + + // ========================================================================== + // commitGraph + // ========================================================================== + + describe("commitGraph", () => { + // --- success path --- + + it("creates multiple nodes + edges in one transaction with one LSN", () => { + const input: CommitGraphInput = { + nodes: [ + { ref: "n1", plane: "intent", kind: "requirement", title: "Req A" }, + { ref: "n2", plane: "intent", kind: "constraint", title: "Con B" }, + ], + edges: [{ category: "boundary", source: "n2", target: "n1" }], + } + + const result = executor.commitGraph(input) + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + + expect(result.lsn).toBe(1) + expect(Object.keys(result.nodes)).toHaveLength(2) + expect(result.edges).toHaveLength(1) + + // Verify DB state + expect(db.select().from(nodes).all()).toHaveLength(2) + expect(db.select().from(edges).all()).toHaveLength(1) + }) + + it("resolves intra-batch refs to real NodeIds", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "a", plane: "intent", kind: "assumption", title: "A1" }, + { + ref: "b", + plane: "intent", + kind: "decision", + title: "D1", + detail: { + chosen_option: "X", + rejected: ["Y"], + rationale: "because", + }, + }, + ], + edges: [{ category: "dependency", source: "a", target: "b" }], + }) + + if (result.status !== "success") throw new Error("unreachable") + const edgeRow = db.select().from(edges).all()[0]! + expect(edgeRow.source_id).toBe(result.nodes["a"]) + expect(edgeRow.target_id).toBe(result.nodes["b"]) + }) + + it("resolves existing-node refs to verified NodeIds", () => { + // Pre-create a node + const pre = executor.createNode({ + plane: "intent", + kind: "goal", + title: "Existing goal", + }) + if (pre.status !== "success") throw new Error("unreachable") + + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "requirement", title: "New req" }, + ], + edges: [ + { + category: "support", + source: { existing: pre.nodeId }, + target: "n1", + stance: "for", + }, + ], + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + const edgeRow = db.select().from(edges).all()[0]! + expect(edgeRow.source_id).toBe(pre.nodeId) + expect(edgeRow.target_id).toBe(result.nodes["n1"]) + }) + + it("returns nodes mapping and edges array in success result", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "x", plane: "intent", kind: "context", title: "Ctx" }, + { ref: "y", plane: "intent", kind: "thesis", title: "Thesis" }, + ], + edges: [], + }) + + if (result.status !== "success") throw new Error("unreachable") + expect(result.nodes["x"]).toBeTypeOf("number") + expect(result.nodes["y"]).toBeTypeOf("number") + expect(result.nodes["x"]).not.toBe(result.nodes["y"]) + expect(result.edges).toEqual([]) + }) + + it("appends one change_log entry for the entire batch", () => { + executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "G1" }, + { ref: "n2", plane: "intent", kind: "goal", title: "G2" }, + ], + edges: [{ category: "association", source: "n1", target: "n2" }], + }) + + const logs = db.select().from(changeLog).all() + expect(logs).toHaveLength(1) + expect(logs[0]!.operation).toBe("commit_graph") + const payload = JSON.parse(logs[0]!.payload) + expect(Object.keys(payload.nodes)).toHaveLength(2) + expect(payload.edges).toHaveLength(1) + }) + + // --- edge structural validation --- + + it("rejects edge with invalid category", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "G" }, + { ref: "n2", plane: "intent", kind: "goal", title: "G2" }, + ], + edges: [{ category: "invented_relation", source: "n1", target: "n2" }], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field.includes("category"))).toBe( + true, + ) + }) + + it("rejects proof edge without stance", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "criterion", title: "Cr" }, + { ref: "n2", plane: "intent", kind: "invariant", title: "Inv" }, + ], + edges: [{ category: "proof", source: "n1", target: "n2" }], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field.includes("stance"))).toBe( + true, + ) + }) + + it("rejects support edge without stance", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "context", title: "Ctx" }, + { ref: "n2", plane: "intent", kind: "requirement", title: "Req" }, + ], + edges: [{ category: "support", source: "n1", target: "n2" }], + }) + + expect(result.status).toBe("structural_illegal") + }) + + it("rejects non-proof/non-support edge with stance", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "assumption", title: "A" }, + { ref: "n2", plane: "intent", kind: "requirement", title: "R" }, + ], + edges: [ + { category: "dependency", source: "n1", target: "n2", stance: "for" }, + ], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field.includes("stance"))).toBe( + true, + ) + }) + + it("rejects edge referencing non-existent existing node", () => { + const result = executor.commitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "G" }], + edges: [ + { category: "dependency", source: { existing: 9999 }, target: "n1" }, + ], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field.includes("source"))).toBe( + true, + ) + }) + + it("rejects edge with unresolvable intra-batch ref", () => { + const result = executor.commitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "G" }], + edges: [ + { category: "dependency", source: "n1", target: "missing_ref" }, + ], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field.includes("target"))).toBe( + true, + ) + }) + + it("rejects self-loop edge", () => { + const result = executor.commitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "G" }], + edges: [{ category: "association", source: "n1", target: "n1" }], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect( + result.diagnostics.some((d) => d.message.includes("self-loop")), + ).toBe(true) + }) + + // --- node validation reuse --- + + it("rejects batch node with invalid kind-for-plane", () => { + const result = executor.commitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "check", title: "Wrong" }], + edges: [], + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics.some((d) => d.field.includes("nodes[0]"))).toBe( + true, + ) + }) + + it("rejects batch decision without detail", () => { + const result = executor.commitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "decision", title: "D" }], + edges: [], + }) + + expect(result.status).toBe("structural_illegal") + }) + + // --- all-or-nothing (I34-L) --- + + it("if any node fails validation, entire batch rejected — nothing written", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "Valid" }, + { ref: "n2", plane: "intent", kind: "check", title: "Invalid kind" }, + ], + edges: [], + }) + + expect(result.status).toBe("structural_illegal") + expect(db.select().from(nodes).all()).toHaveLength(0) + const [clock] = db.select().from(graphClock).all() + expect(clock!.lsn).toBe(0) + }) + + it("if any edge fails validation, no nodes written", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "Valid goal" }, + { ref: "n2", plane: "intent", kind: "context", title: "Valid ctx" }, + ], + edges: [ + { category: "proof", source: "n1", target: "n2" }, // missing stance + ], + }) + + expect(result.status).toBe("structural_illegal") + // Transaction rolled back — no nodes either + expect(db.select().from(nodes).all()).toHaveLength(0) + const [clock] = db.select().from(graphClock).all() + expect(clock!.lsn).toBe(0) + }) + + it("diagnostics include which entry failed", () => { + const result = executor.commitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "OK" }], + edges: [ + { category: "dependency", source: "n1", target: { existing: 9999 } }, + ], + }) + + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect( + result.diagnostics.some((d) => d.field.startsWith("edges[0]")), + ).toBe(true) + }) + + // --- edge cases --- + + it("edge-only batch between existing nodes", () => { + const a = executor.createNode({ + plane: "intent", + kind: "requirement", + title: "R1", + }) + const b = executor.createNode({ + plane: "intent", + kind: "assumption", + title: "A1", + }) + if (a.status !== "success" || b.status !== "success") + throw new Error("unreachable") + + const result = executor.commitGraph({ + nodes: [], + edges: [ + { + category: "dependency", + source: { existing: b.nodeId }, + target: { existing: a.nodeId }, + }, + ], + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + expect(Object.keys(result.nodes)).toHaveLength(0) + expect(result.edges).toHaveLength(1) + }) + + it("node-only batch (no edges)", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "context", title: "C1" }, + { ref: "n2", plane: "intent", kind: "context", title: "C2" }, + ], + edges: [], + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + expect(Object.keys(result.nodes)).toHaveLength(2) + expect(result.edges).toEqual([]) + }) + + it("empty batch → structural_illegal", () => { + const result = executor.commitGraph({ nodes: [], edges: [] }) + expect(result.status).toBe("structural_illegal") + }) + + // --- mixed refs --- + + it("edges can mix intra-batch source with existing target", () => { + const pre = executor.createNode({ + plane: "intent", + kind: "goal", + title: "Existing", + }) + if (pre.status !== "success") throw new Error("unreachable") + + const result = executor.commitGraph({ + nodes: [ + { ref: "new", plane: "intent", kind: "requirement", title: "New" }, + ], + edges: [ + { + category: "realization", + source: { existing: pre.nodeId }, + target: "new", + }, + ], + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + const edgeRow = db.select().from(edges).all()[0]! + expect(edgeRow.source_id).toBe(pre.nodeId) + expect(edgeRow.target_id).toBe(result.nodes["new"]) + }) + + // --- LSN behavior --- + + it("uses one LSN for the entire batch (not per-entity)", () => { + const result = executor.commitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "G1" }, + { ref: "n2", plane: "intent", kind: "goal", title: "G2" }, + ], + edges: [{ category: "association", source: "n1", target: "n2" }], + }) + + if (result.status !== "success") throw new Error("unreachable") + const allNodes = db.select().from(nodes).all() + const allEdges = db.select().from(edges).all() + // All entities share the same LSN + for (const n of allNodes) { + expect(n.created_at_lsn).toBe(result.lsn) + } + for (const e of allEdges) { + expect(e.created_at_lsn).toBe(result.lsn) + } + }) + }) + + // --- createReconciliationNeed --- + + describe("createReconciliationNeed", () => { + it("creates a recon need targeting an edge and returns success with id and lsn", () => { + // Seed a node and edge first + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [{ category: "dependency", source: "r1", target: "a1" }], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + const edgeId = batch.edges[0]! + + const result = executor.createReconciliationNeed({ + target: { kind: "edge", edgeId }, + needKind: "edge_revalidation", + reason: "upstream assumption changed", + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + expect(result.id).toBeTypeOf("number") + expect(result.lsn).toBeTypeOf("number") + }) + + it("creates a recon need targeting a node pair", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "r2", plane: "intent", kind: "requirement", title: "R2" }, + ], + edges: [], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + const aId = batch.nodes["r1"]! + const bId = batch.nodes["r2"]! + + const result = executor.createReconciliationNeed({ + target: { kind: "node_pair", aId, bId }, + needKind: "possible_duplicate", + }) + + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + expect(result.id).toBeTypeOf("number") + }) + + it("rejects edge target with non-existent edgeId", () => { + const result = executor.createReconciliationNeed({ + target: { kind: "edge", edgeId: 999 }, + needKind: "edge_revalidation", + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics[0]!.field).toBe("target.edgeId") + }) + + it("rejects node_pair target with non-existent nodeId", () => { + const n = executor.createNode({ + plane: "intent", + kind: "goal", + title: "G1", + }) + expect(n.status).toBe("success") + if (n.status !== "success") throw new Error("unreachable") + + const result = executor.createReconciliationNeed({ + target: { kind: "node_pair", aId: n.nodeId, bId: 999 }, + needKind: "possible_relation", + }) + + expect(result.status).toBe("structural_illegal") + if (result.status !== "structural_illegal") throw new Error("unreachable") + expect(result.diagnostics[0]!.field).toBe("target.bId") + }) + + it("allocates a new LSN for each recon need", () => { + const n = executor.createNode({ + plane: "intent", + kind: "goal", + title: "G1", + }) + expect(n.status).toBe("success") + if (n.status !== "success") throw new Error("unreachable") + const n2 = executor.createNode({ + plane: "intent", + kind: "goal", + title: "G2", + }) + expect(n2.status).toBe("success") + if (n2.status !== "success") throw new Error("unreachable") + + const r1 = executor.createReconciliationNeed({ + target: { kind: "node_pair", aId: n.nodeId, bId: n2.nodeId }, + needKind: "possible_relation", + }) + expect(r1.status).toBe("success") + if (r1.status !== "success") throw new Error("unreachable") + + const r2 = executor.createReconciliationNeed({ + target: { kind: "node_pair", aId: n.nodeId, bId: n2.nodeId }, + needKind: "semantic_conflict", + }) + expect(r2.status).toBe("success") + if (r2.status !== "success") throw new Error("unreachable") + + expect(r2.lsn).toBeGreaterThan(r1.lsn) + }) + }) + + // --- resolveReconciliationNeed --- + + describe("resolveReconciliationNeed", () => { + it("resolves an open need and records resolvedAtLsn", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [{ category: "dependency", source: "r1", target: "a1" }], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const create = executor.createReconciliationNeed({ + target: { kind: "edge", edgeId: batch.edges[0]! }, + needKind: "edge_revalidation", + }) + expect(create.status).toBe("success") + if (create.status !== "success") throw new Error("unreachable") + + const resolve = executor.resolveReconciliationNeed(create.id) + expect(resolve.status).toBe("success") + if (resolve.status !== "success") throw new Error("unreachable") + expect(resolve.lsn).toBeGreaterThan(create.lsn) + }) + + it("rejects non-existent need id", () => { + const result = executor.resolveReconciliationNeed(999) + expect(result.status).toBe("structural_illegal") + }) + + it("rejects already-resolved need", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [{ category: "dependency", source: "r1", target: "a1" }], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const create = executor.createReconciliationNeed({ + target: { kind: "edge", edgeId: batch.edges[0]! }, + needKind: "edge_revalidation", + }) + expect(create.status).toBe("success") + if (create.status !== "success") throw new Error("unreachable") + + const resolve1 = executor.resolveReconciliationNeed(create.id) + expect(resolve1.status).toBe("success") + + const resolve2 = executor.resolveReconciliationNeed(create.id) + expect(resolve2.status).toBe("structural_illegal") + if (resolve2.status !== "structural_illegal") + throw new Error("unreachable") + expect(resolve2.diagnostics[0]!.message).toContain("already resolved") + }) + }) +}) diff --git a/src/graph/command-executor.ts b/src/graph/command-executor.ts new file mode 100644 index 000000000..036b6764f --- /dev/null +++ b/src/graph/command-executor.ts @@ -0,0 +1,843 @@ +/** + * CommandExecutor — the single public mutation boundary for graph truth. + * + * SPEC: D4-L (one shared mutation surface), D20-L (command execution owns + * authority seam), D16-L (one-transaction-per-commit, no bypass), D52-L + * (graph/ imports db/, no other layer imports db/). + * + * Every graph mutation routes through this class. The executor owns: + * - structural validation + * - one SQLite transaction per command + * - monotonic LSN allocation from graph_clock + * - change_log append + * - structured result return + * + * The result contract already includes all discriminants (success, + * structural_illegal, needs_human, policy_blocked, version_conflict) + * even though pre-M6 policy classification is minimal. + */ + +import { eq, inArray, sql } from "drizzle-orm" + +import type { BrunchDb } from "../db/connection.js" +import * as schema from "../db/schema.js" +import type { EdgeCategory, EdgeStance } from "./schema/edges.js" +import type { NodeBasis, NodePlane } from "./schema/nodes.js" + +// --------------------------------------------------------------------------- +// Result types +// --------------------------------------------------------------------------- + +/** A single validation problem discovered during structural checks. */ +export interface Diagnostic { + readonly field: string + readonly message: string +} + +/** Successful command execution. */ +export interface CommandSuccess { + readonly status: "success" + readonly nodeId: number + readonly lsn: number +} + +/** Structurally invalid input — validation failed before any write. */ +export interface StructuralIllegal { + readonly status: "structural_illegal" + readonly diagnostics: readonly Diagnostic[] +} + +/** Action requires human confirmation (M6 placeholder). */ +export interface NeedsHuman { + readonly status: "needs_human" +} + +/** Action blocked by authority policy (M6 placeholder). */ +export interface PolicyBlocked { + readonly status: "policy_blocked" +} + +/** Optimistic concurrency conflict (M6 placeholder). */ +export interface VersionConflict { + readonly status: "version_conflict" +} + +/** Successful commitGraph batch execution. */ +export interface CommitGraphSuccess { + readonly status: "success" + readonly lsn: number + readonly nodes: Readonly> + readonly edges: readonly number[] +} + +/** Successful reconciliation-need creation. */ +export interface ReconNeedSuccess { + readonly status: "success" + readonly id: number + readonly lsn: number +} + +/** Successful reconciliation-need resolution. */ +export interface ReconNeedResolveSuccess { + readonly status: "success" + readonly lsn: number +} + +/** Union of all possible command results. */ +export type CommandResult = CommandSuccess | CommitGraphSuccess | ReconNeedSuccess | ReconNeedResolveSuccess | StructuralIllegal | NeedsHuman | PolicyBlocked | VersionConflict + +/** Result of a createNode command. */ +export type CreateNodeResult = CommandSuccess | StructuralIllegal + +/** Result of a commitGraph command. */ +export type CommitGraphResult = CommitGraphSuccess | StructuralIllegal + +/** Result of a createReconciliationNeed command. */ +export type CreateReconNeedResult = ReconNeedSuccess | StructuralIllegal + +/** Result of a resolveReconciliationNeed command. */ +export type ResolveReconNeedResult = ReconNeedResolveSuccess | StructuralIllegal + +// --------------------------------------------------------------------------- +// Input types +// --------------------------------------------------------------------------- + +/** Input for creating a single graph node. */ +export interface CreateNodeInput { + readonly plane: NodePlane + readonly kind: string + readonly title: string + readonly body?: string | undefined + readonly basis?: NodeBasis | undefined + readonly source?: string | undefined + readonly detail?: unknown +} + +// --------------------------------------------------------------------------- +// Reconciliation-need input types +// --------------------------------------------------------------------------- + +/** Target for a reconciliation need — edge or node pair. */ +export type ReconNeedTargetEdge = { + readonly kind: "edge" + readonly edgeId: number +} + +/** Target for a reconciliation need — node pair. */ +export type ReconNeedTargetNodePair = { + readonly kind: "node_pair" + readonly aId: number + readonly bId: number +} + +/** Target for a reconciliation need. */ +export type ReconNeedTarget = ReconNeedTargetEdge | ReconNeedTargetNodePair + +/** Input for creating a reconciliation need. */ +export interface CreateReconNeedInput { + readonly target: ReconNeedTarget + readonly needKind: string + readonly reason?: string | undefined +} + +// --------------------------------------------------------------------------- +// Batch input types (commitGraph) +// --------------------------------------------------------------------------- + +/** Reference to a node endpoint in a batch edge. */ +export type BatchEdgeRef = string | { readonly existing: number } + +/** A node to create inside a commitGraph batch. */ +export interface BatchNodeInput { + readonly ref: string + readonly plane: NodePlane + readonly kind: string + readonly title: string + readonly body?: string | undefined + readonly basis?: NodeBasis | undefined + readonly source?: string | undefined + readonly detail?: unknown +} + +/** An edge to create inside a commitGraph batch. */ +export interface BatchEdgeInput { + readonly category: string + readonly source: BatchEdgeRef + readonly target: BatchEdgeRef + readonly stance?: string | undefined + readonly basis?: NodeBasis | undefined + readonly rationale?: string | undefined +} + +/** Input for the commitGraph atomic batch mutation. */ +export interface CommitGraphInput { + readonly nodes: readonly BatchNodeInput[] + readonly edges: readonly BatchEdgeInput[] +} + +// --------------------------------------------------------------------------- +// Validation +// --------------------------------------------------------------------------- + +const VALID_KINDS_BY_PLANE: Record = { + intent: schema.INTENT_KINDS as unknown as string[], + oracle: schema.ORACLE_KINDS as unknown as string[], + design: schema.DESIGN_KINDS as unknown as string[], + plan: schema.PLAN_KINDS as unknown as string[], +} + +const KINDS_REQUIRING_DETAIL = new Set(["decision", "term"]) + +function validateCreateNode(input: CreateNodeInput): Diagnostic[] { + const diagnostics: Diagnostic[] = [] + + // Title must be non-empty + if (!input.title.trim()) { + diagnostics.push({ field: "title", message: "title must be non-empty" }) + } + + // Kind must be valid for the given plane + const validKinds = VALID_KINDS_BY_PLANE[input.plane] + if (!validKinds?.includes(input.kind)) { + diagnostics.push({ + field: "kind", + message: `"${input.kind}" is not a valid kind for plane "${input.plane}"`, + }) + return diagnostics // can't validate detail if kind is wrong + } + + // Detail requirement: decision and term REQUIRE detail + if (KINDS_REQUIRING_DETAIL.has(input.kind) && input.detail == null) { + diagnostics.push({ + field: "detail", + message: `"${input.kind}" nodes require a detail object`, + }) + return diagnostics + } + + // Detail prohibition: all other kinds must NOT have detail + if (!KINDS_REQUIRING_DETAIL.has(input.kind) && input.detail != null) { + diagnostics.push({ + field: "detail", + message: `"${input.kind}" nodes must not have a detail object`, + }) + return diagnostics + } + + // Validate detail shape per kind + if (input.kind === "decision" && input.detail != null) { + validateDecisionDetail(input.detail, diagnostics) + } + if (input.kind === "term" && input.detail != null) { + validateTermDetail(input.detail, diagnostics) + } + + return diagnostics +} + +function validateDecisionDetail( + detail: unknown, + diagnostics: Diagnostic[], +): void { + if (typeof detail !== "object" || detail === null) { + diagnostics.push({ field: "detail", message: "must be an object" }) + return + } + + const d = detail as Record + const knownFields = new Set(["chosen_option", "rejected", "rationale"]) + + if (typeof d["chosen_option"] !== "string") { + diagnostics.push({ + field: "detail.chosen_option", + message: "required string", + }) + } + + if ( + !Array.isArray(d["rejected"]) || + d["rejected"].length < 1 || + !d["rejected"].every((r) => typeof r === "string") + ) { + diagnostics.push({ + field: "detail.rejected", + message: "required non-empty string array", + }) + } + + if (typeof d["rationale"] !== "string") { + diagnostics.push({ field: "detail.rationale", message: "required string" }) + } + + // Closed validation: reject unknown fields + for (const key of Object.keys(d)) { + if (!knownFields.has(key)) { + diagnostics.push({ field: `detail.${key}`, message: "unknown field" }) + } + } +} + +function validateTermDetail(detail: unknown, diagnostics: Diagnostic[]): void { + if (typeof detail !== "object" || detail === null) { + diagnostics.push({ field: "detail", message: "must be an object" }) + return + } + + const d = detail as Record + const knownFields = new Set(["definition", "aliases"]) + + if (typeof d["definition"] !== "string") { + diagnostics.push({ + field: "detail.definition", + message: "required string", + }) + } + + if ( + d["aliases"] != null && + (!Array.isArray(d["aliases"]) || + !d["aliases"].every((a) => typeof a === "string")) + ) { + diagnostics.push({ + field: "detail.aliases", + message: "must be a string array if present", + }) + } + + // Closed validation: reject unknown fields + for (const key of Object.keys(d)) { + if (!knownFields.has(key)) { + diagnostics.push({ field: `detail.${key}`, message: "unknown field" }) + } + } +} + +// --------------------------------------------------------------------------- +// Edge validation +// --------------------------------------------------------------------------- + +const VALID_CATEGORIES = schema.EDGE_CATEGORIES as unknown as string[] +const STANCE_REQUIRED_CATEGORIES = new Set(["proof", "support"]) +const VALID_STANCES = schema.EDGE_STANCES as unknown as string[] + +interface ResolvedEdge { + sourceId: number + targetId: number + category: EdgeCategory + stance: EdgeStance | null + basis: NodeBasis + rationale: string | null +} + +interface EdgeValidationResult { + diagnostics: Diagnostic[] + resolved?: ResolvedEdge +} + +function validateAndResolveBatchEdge( + input: BatchEdgeInput, + index: number, + refMap: ReadonlyMap, + existingNodeIds: ReadonlySet, +): EdgeValidationResult { + const diagnostics: Diagnostic[] = [] + const p = `edges[${index}]` + + // Category must be in the closed set + if (!VALID_CATEGORIES.includes(input.category)) { + diagnostics.push({ + field: `${p}.category`, + message: `"${input.category}" is not a valid edge category`, + }) + return { diagnostics } + } + + // Stance: required iff proof/support, invalid otherwise + const stanceRequired = STANCE_REQUIRED_CATEGORIES.has(input.category) + if (stanceRequired && input.stance == null) { + diagnostics.push({ + field: `${p}.stance`, + message: `stance is required for "${input.category}" edges`, + }) + } + if (!stanceRequired && input.stance != null) { + diagnostics.push({ + field: `${p}.stance`, + message: `stance is not allowed for "${input.category}" edges`, + }) + } + if (input.stance != null && !VALID_STANCES.includes(input.stance)) { + diagnostics.push({ + field: `${p}.stance`, + message: `"${input.stance}" is not a valid stance`, + }) + } + + // Resolve source ref + let resolvedSourceId: number | undefined + if (typeof input.source === "string") { + resolvedSourceId = refMap.get(input.source) + if (resolvedSourceId === undefined) { + diagnostics.push({ + field: `${p}.source`, + message: `unresolvable intra-batch ref "${input.source}"`, + }) + } + } else { + resolvedSourceId = input.source.existing + if (!existingNodeIds.has(resolvedSourceId)) { + diagnostics.push({ + field: `${p}.source`, + message: `existing node ${resolvedSourceId} not found`, + }) + } + } + + // Resolve target ref + let resolvedTargetId: number | undefined + if (typeof input.target === "string") { + resolvedTargetId = refMap.get(input.target) + if (resolvedTargetId === undefined) { + diagnostics.push({ + field: `${p}.target`, + message: `unresolvable intra-batch ref "${input.target}"`, + }) + } + } else { + resolvedTargetId = input.target.existing + if (!existingNodeIds.has(resolvedTargetId)) { + diagnostics.push({ + field: `${p}.target`, + message: `existing node ${resolvedTargetId} not found`, + }) + } + } + + // Self-loop check (only if both resolved) + if ( + resolvedSourceId !== undefined && + resolvedTargetId !== undefined && + resolvedSourceId === resolvedTargetId + ) { + diagnostics.push({ + field: p, + message: "self-loop: source and target resolve to the same node", + }) + } + + if (diagnostics.length > 0) return { diagnostics } + + return { + diagnostics, + resolved: { + sourceId: resolvedSourceId!, + targetId: resolvedTargetId!, + category: input.category as EdgeCategory, + stance: input.stance as EdgeStance ?? null, + basis: input.basis as NodeBasis ?? "explicit", + rationale: input.rationale ?? null, + }, + } +} + +/** Thrown inside a transaction to trigger rollback on edge validation failure. */ +class BatchValidationError extends Error { + constructor(readonly diagnostics: readonly Diagnostic[]) { + super("batch validation failed") + } +} + +// --------------------------------------------------------------------------- +// CommandExecutor +// --------------------------------------------------------------------------- + +export class CommandExecutor { + constructor(private readonly db: BrunchDb) {} + + /** + * Create a single graph node. + * + * Validates structurally, then executes inside one transaction: + * allocate LSN → insert node → append change_log → return result. + * + * On validation failure, nothing is written. + */ + createNode(input: CreateNodeInput): CreateNodeResult { + const diagnostics = validateCreateNode(input) + if (diagnostics.length > 0) { + return { status: "structural_illegal", diagnostics } + } + + return this.db.transaction((tx) => { + // 1. Allocate LSN (atomic increment) + const clock = tx + .update(schema.graphClock) + .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) + .where(eq(schema.graphClock.id, 1)) + .returning() + .get() + const lsn = clock!.lsn + + // 2. Insert node + const node = tx + .insert(schema.nodes) + .values({ + plane: input.plane, + kind: input.kind, + title: input.title, + body: input.body ?? null, + basis: input.basis ?? "explicit", + source: input.source ?? null, + detail: input.detail != null ? JSON.stringify(input.detail) : null, + created_at_lsn: lsn, + updated_at_lsn: lsn, + }) + .returning() + .get() + const nodeId = node!.id + + // 3. Append change_log + tx.insert(schema.changeLog) + .values({ + lsn, + operation: "create_node", + payload: JSON.stringify({ + nodeId, + plane: input.plane, + kind: input.kind, + }), + }) + .run() + + return { status: "success" as const, nodeId, lsn } + }) + } + + /** + * Atomic batch creation of nodes and edges (D53-L). + * + * One transaction, one LSN. Intra-batch refs (strings) resolve to + * just-inserted NodeIds; existing refs ({ existing: id }) are verified + * against the database. All-or-nothing: if any entry fails structural + * validation, the entire batch is rejected (I34-L). + */ + commitGraph(input: CommitGraphInput): CommitGraphResult { + // Empty batch is structural_illegal + if (input.nodes.length === 0 && input.edges.length === 0) { + return { + status: "structural_illegal", + diagnostics: [ + { field: "batch", message: "empty batch — nothing to commit" }, + ], + } + } + + // --- Pre-transaction: validate all batch nodes (pure checks) --- + const preDiagnostics: Diagnostic[] = [] + const seenRefs = new Set() + + for (let i = 0; i < input.nodes.length; i++) { + const bn = input.nodes[i]! + + // Duplicate ref check + if (seenRefs.has(bn.ref)) { + preDiagnostics.push({ + field: `nodes[${i}].ref`, + message: `duplicate batch ref "${bn.ref}"`, + }) + } + seenRefs.add(bn.ref) + + // Structural node validation (reuse) + for (const d of validateCreateNode(bn)) { + preDiagnostics.push({ + field: `nodes[${i}].${d.field}`, + message: d.message, + }) + } + } + + if (preDiagnostics.length > 0) { + return { status: "structural_illegal", diagnostics: preDiagnostics } + } + + // --- Transaction: insert nodes, resolve refs, validate + insert edges --- + try { + return this.db.transaction((tx) => { + // 1. Allocate ONE LSN + const clock = tx + .update(schema.graphClock) + .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) + .where(eq(schema.graphClock.id, 1)) + .returning() + .get() + const lsn = clock!.lsn + + // 2. Insert all nodes, build ref → id map + const refMap = new Map() + for (const bn of input.nodes) { + const row = tx + .insert(schema.nodes) + .values({ + plane: bn.plane, + kind: bn.kind, + title: bn.title, + body: bn.body ?? null, + basis: bn.basis ?? "explicit", + source: bn.source ?? null, + detail: bn.detail != null ? JSON.stringify(bn.detail) : null, + created_at_lsn: lsn, + updated_at_lsn: lsn, + }) + .returning() + .get() + refMap.set(bn.ref, row!.id) + } + + // 3. Collect and verify existing-node references + const existingRefs = new Set() + for (const edge of input.edges) { + if (typeof edge.source !== "string") + existingRefs.add(edge.source.existing) + if (typeof edge.target !== "string") + existingRefs.add(edge.target.existing) + } + + const verifiedExisting = new Set() + if (existingRefs.size > 0) { + const rows = tx + .select({ id: schema.nodes.id }) + .from(schema.nodes) + .where(inArray(schema.nodes.id, [...existingRefs])) + .all() + for (const row of rows) verifiedExisting.add(row.id) + } + + // 4. Validate and resolve all edges + const edgeDiagnostics: Diagnostic[] = [] + const resolvedEdges: ResolvedEdge[] = [] + + for (let i = 0; i < input.edges.length; i++) { + const result = validateAndResolveBatchEdge( + input.edges[i]!, + i, + refMap, + verifiedExisting, + ) + edgeDiagnostics.push(...result.diagnostics) + if (result.resolved) resolvedEdges.push(result.resolved) + } + + if (edgeDiagnostics.length > 0) { + throw new BatchValidationError(edgeDiagnostics) + } + + // 5. Insert all edges + const edgeIds: number[] = [] + for (const re of resolvedEdges) { + const row = tx + .insert(schema.edges) + .values({ + category: re.category, + source_id: re.sourceId, + target_id: re.targetId, + stance: re.stance, + basis: re.basis, + rationale: re.rationale, + created_at_lsn: lsn, + updated_at_lsn: lsn, + }) + .returning() + .get() + edgeIds.push(row!.id) + } + + // 6. Append one change_log entry for the entire batch + tx.insert(schema.changeLog) + .values({ + lsn, + operation: "commit_graph", + payload: JSON.stringify({ + nodes: Object.fromEntries(refMap), + edges: edgeIds, + }), + }) + .run() + + return { + status: "success" as const, + lsn, + nodes: Object.fromEntries(refMap), + edges: edgeIds, + } + }) + } catch (e) { + if (e instanceof BatchValidationError) { + return { status: "structural_illegal", diagnostics: e.diagnostics } + } + throw e + } + } + + /** + * Create a reconciliation need. + * + * Validates that the target (edge or node pair) exists, then inserts + * inside one transaction with LSN allocation and change_log append. + */ + createReconciliationNeed(input: CreateReconNeedInput): CreateReconNeedResult { + // Validate target references exist + return this.db.transaction((tx) => { + const diagnostics: Diagnostic[] = [] + + if (input.target.kind === "edge") { + const row = tx + .select({ id: schema.edges.id }) + .from(schema.edges) + .where(eq(schema.edges.id, input.target.edgeId)) + .get() + if (!row) { + diagnostics.push({ + field: "target.edgeId", + message: `edge ${input.target.edgeId} does not exist`, + }) + } + } else { + const aRow = tx + .select({ id: schema.nodes.id }) + .from(schema.nodes) + .where(eq(schema.nodes.id, input.target.aId)) + .get() + if (!aRow) { + diagnostics.push({ + field: "target.aId", + message: `node ${input.target.aId} does not exist`, + }) + } + const bRow = tx + .select({ id: schema.nodes.id }) + .from(schema.nodes) + .where(eq(schema.nodes.id, input.target.bId)) + .get() + if (!bRow) { + diagnostics.push({ + field: "target.bId", + message: `node ${input.target.bId} does not exist`, + }) + } + } + + if (diagnostics.length > 0) { + return { status: "structural_illegal" as const, diagnostics } + } + + // Allocate LSN + const clock = tx + .update(schema.graphClock) + .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) + .where(eq(schema.graphClock.id, 1)) + .returning() + .get() + const lsn = clock!.lsn + + // Insert reconciliation need + const row = tx + .insert(schema.reconciliationNeed) + .values({ + target_kind: input.target.kind, + target_edge_id: + input.target.kind === "edge" ? input.target.edgeId : null, + target_a_id: + input.target.kind === "node_pair" ? input.target.aId : null, + target_b_id: + input.target.kind === "node_pair" ? input.target.bId : null, + kind: input.needKind, + reason: input.reason ?? null, + created_at_lsn: lsn, + }) + .returning() + .get() + + // Append change_log + tx.insert(schema.changeLog) + .values({ + lsn, + operation: "create_reconciliation_need", + payload: JSON.stringify({ + id: row!.id, + target: input.target, + kind: input.needKind, + }), + }) + .run() + + return { status: "success" as const, id: row!.id, lsn } + }) + } + + /** + * Resolve an open reconciliation need. + * + * Sets status to "resolved" and records the resolvedAtLsn. + * Rejects if the need does not exist or is already resolved. + */ + resolveReconciliationNeed(id: number): ResolveReconNeedResult { + return this.db.transaction((tx) => { + const existing = tx + .select() + .from(schema.reconciliationNeed) + .where(eq(schema.reconciliationNeed.id, id)) + .get() + + if (!existing) { + return { + status: "structural_illegal" as const, + diagnostics: [ + { + field: "id", + message: `reconciliation need ${id} does not exist`, + }, + ], + } + } + + if (existing.status === "resolved") { + return { + status: "structural_illegal" as const, + diagnostics: [ + { + field: "id", + message: `reconciliation need ${id} is already resolved`, + }, + ], + } + } + + // Allocate LSN + const clock = tx + .update(schema.graphClock) + .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) + .where(eq(schema.graphClock.id, 1)) + .returning() + .get() + const lsn = clock!.lsn + + // Update status + tx.update(schema.reconciliationNeed) + .set({ status: "resolved", resolved_at_lsn: lsn }) + .where(eq(schema.reconciliationNeed.id, id)) + .run() + + // Append change_log + tx.insert(schema.changeLog) + .values({ + lsn, + operation: "resolve_reconciliation_need", + payload: JSON.stringify({ id }), + }) + .run() + + return { status: "success" as const, lsn } + }) + } +} diff --git a/src/graph/index.ts b/src/graph/index.ts new file mode 100644 index 000000000..6273eeecc --- /dev/null +++ b/src/graph/index.ts @@ -0,0 +1,86 @@ +/** + * Public exports for the Brunch graph layer. + * + * Canonical reference: docs/design/GRAPH_MODEL.md + * + * Phase 1: edges, edge policy, reconciliation-need. + * Phase 2: node type definitions. + * M4 skeleton: CommandExecutor + result types. + */ + +export type { EdgeId, Lsn, NodeId } from "./atoms.js" + +export type { + EdgeBasis, + EdgeCategory, + EdgeStance, + GraphEdge, +} from "./schema/edges.js" + +export type { + DecisionDetail, + DesignKind, + GraphNode, + IntentKind, + IntentKindCategory, + NodeBasis, + NodeDetail, + NodeKind, + NodePlane, + OracleKind, + PlanKind, + TermDetail, +} from "./schema/nodes.js" + +export { intentKindCategory } from "./schema/nodes.js" + +export type { + ReconciliationNeed, + ReconciliationNeedKind, + ReconciliationNeedTarget, +} from "./schema/reconciliation-need.js" + +export { + CATEGORY_POLICY, + type CategoryPolicy, + type ProjectionEffect, + type ReconNeedTrigger, +} from "./policy/category-policy.js" + +export { + getGraphOverview, + getNodeNeighborhood, + getOpenReconciliationNeeds, +} from "./snapshot.js" +export type { + GraphOverview, + NeighborhoodOptions, + NeighborhoodNotFound, + NeighborhoodResult, + NeighborhoodSuccess, +} from "./snapshot.js" + +export { CommandExecutor } from "./command-executor.js" +export type { + BatchEdgeInput, + BatchEdgeRef, + BatchNodeInput, + CommandResult, + CommandSuccess, + CommitGraphInput, + CommitGraphResult, + CommitGraphSuccess, + CreateNodeInput, + CreateNodeResult, + CreateReconNeedInput, + CreateReconNeedResult, + Diagnostic, + NeedsHuman, + PolicyBlocked, + ReconNeedResolveSuccess, + ReconNeedSuccess, + ReconNeedTarget, + ResolveReconNeedResult, + StructuralIllegal, + VersionConflict, +} from "./command-executor.js" diff --git a/src/graph/policy/category-policy.ts b/src/graph/policy/category-policy.ts new file mode 100644 index 000000000..76fbd8227 --- /dev/null +++ b/src/graph/policy/category-policy.ts @@ -0,0 +1,92 @@ +/** + * Per-edge-category policy table. + * + * Canonical reference: docs/design/GRAPH_MODEL.md §"Per-category policy" + * + * This table replaces the prior multi-axis per-relation policy + * registry. Only the axes that have a present reader in M4 or M5 + * are encoded here: + * + * - `cascadeOnSourceChange` — automatic block / mark-stale on the + * dependent (assumption-invalidation + * cascade). Only `dependency` cascades. + * - `reconNeedOnSourceChange` — generate a ReconciliationNeed pointing + * at the edge. `"advisory"` = generated + * only if a coherence rule asks for it; + * `true` = generated unconditionally. + * - `criteriaHelpSignal` — the interviewer uses this edge when + * suggesting criteria for the target + * node ("requirement with no `proof` + * incoming → suggest criterion"). + * - `projectionEffect` — non-default effect on snapshot / + * neighborhood builders. `"none"` means + * the edge is rendered ordinarily. + * + * Phase 1 lock-and-materialize: data only. The CommandExecutor, + * coherence triggers, projection builders, and interviewer prompts + * consume this table in subsequent M4/M5 slices. + */ + +import type { EdgeCategory } from "../schema/edges.js" + +export type ReconNeedTrigger = false | "advisory" | true + +export type ProjectionEffect = "none" | "hide_predecessor_from_active_context" + +export interface CategoryPolicy { + readonly cascadeOnSourceChange: boolean + readonly reconNeedOnSourceChange: ReconNeedTrigger + readonly criteriaHelpSignal: boolean + readonly projectionEffect: ProjectionEffect +} + +export const CATEGORY_POLICY: Readonly> = { + dependency: { + cascadeOnSourceChange: true, + reconNeedOnSourceChange: true, + criteriaHelpSignal: false, + projectionEffect: "none", + }, + proof: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: "advisory", + criteriaHelpSignal: true, + projectionEffect: "none", + }, + support: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: "advisory", + criteriaHelpSignal: false, + projectionEffect: "none", + }, + realization: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: "advisory", + criteriaHelpSignal: false, + projectionEffect: "none", + }, + boundary: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: true, + criteriaHelpSignal: false, + projectionEffect: "none", + }, + composition: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: false, + criteriaHelpSignal: false, + projectionEffect: "none", + }, + association: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: false, + criteriaHelpSignal: false, + projectionEffect: "none", + }, + supersession: { + cascadeOnSourceChange: false, + reconNeedOnSourceChange: false, + criteriaHelpSignal: false, + projectionEffect: "hide_predecessor_from_active_context", + }, +} diff --git a/src/graph/schema/edges.ts b/src/graph/schema/edges.ts new file mode 100644 index 000000000..35df26258 --- /dev/null +++ b/src/graph/schema/edges.ts @@ -0,0 +1,80 @@ +/** + * Graph edge type definitions. + * + * Canonical reference: docs/design/GRAPH_MODEL.md + * Supersedes: docs/architecture/pi-seam-extensions.md §"Edge types" + * (the prior named-relation catalogue) + * + * Phase 1 lock-and-materialize: type definitions only. + * Drizzle table definitions, structural validators, and the + * agent-facing link* command surface land with subsequent M4/M5 slices. + */ + +import { EDGE_CATEGORIES, EDGE_STANCES, NODE_BASES } from "../../db/schema.js" +import type { EdgeId, Lsn, NodeId } from "../atoms.js" + +/** + * Closed set of structural edge categories. + * + * Derived from `db/schema.ts` — the single enum source. + * + * - `dependency` dependency → dependent hard upstream; cascade + * - `proof` oracle → claim witness or refutation (stance required) + * - `support` support → claim motivation / rationale (stance required) + * - `realization` abstract → concrete expression / implementation + * - `boundary` boundary → subject scope / constraint / exclusion + * - `composition` whole → part containment / decomposition + * - `supersession` successor → predecessor replacement lineage (acyclic) + * - `association` peer ↔ peer weak relatedness (symmetric) + */ +export type EdgeCategory = typeof EDGE_CATEGORIES[number] + +/** + * Polarity for stance-bearing edges. + * + * Required for `proof` and `support`. + * Invalid (must be omitted) for every other category. + */ +export type EdgeStance = typeof EDGE_STANCES[number] + +/** + * How an edge entered graph truth. + * + * `explicit` is a direct user statement; `accepted_review_set` is a + * batch acceptance through `acceptReviewSet` (D27-L). Inferred edges + * do NOT live in graph truth — they live in structured-exchange + * preface or `capture_*` analysis until promoted through a review set + * (D47-L, D50-L). + */ +export type EdgeBasis = typeof NODE_BASES[number] + +// EdgeProvenance retired — change_log owns the full audit trail. + +/** + * A structurally-typed edge in the Brunch graph. + * + * Immutability after acceptance: + * - `category`, `sourceId`, `targetId`, `stance` are immutable. + * - `rationale` may be updated (advances `updatedAtLsn`). + * - To change category: delete and recreate. + * + * Stance: + * - REQUIRED iff `category` is `"proof"` or `"support"`. + * - INVALID (must be omitted) for every other category. + * - Structural validators in the CommandExecutor enforce this. + * + * No `status` field: accepted graph edges are present-or-absent. + * Stale edges surface as `ReconciliationNeed` records pointing at + * the edge (see `src/graph/schema/reconciliation-need.ts`). + */ +export interface GraphEdge { + readonly id: EdgeId + readonly category: EdgeCategory + readonly sourceId: NodeId + readonly targetId: NodeId + readonly stance?: EdgeStance + readonly basis: EdgeBasis + readonly rationale?: string + readonly createdAtLsn: Lsn + readonly updatedAtLsn: Lsn +} diff --git a/src/graph/schema/nodes.ts b/src/graph/schema/nodes.ts new file mode 100644 index 000000000..9cce08be1 --- /dev/null +++ b/src/graph/schema/nodes.ts @@ -0,0 +1,143 @@ +/** + * Graph node type definitions. + * + * Canonical reference: docs/design/GRAPH_MODEL.md + * + * Phase 2 lock-and-materialize: type definitions only. + * Drizzle table definitions, structural validators, and the + * agent-facing node command surface land with subsequent slices. + */ + +import { + DESIGN_KINDS, + INTENT_KINDS, + NODE_BASES, + ORACLE_KINDS, + PLAN_KINDS, +} from "../../db/schema.js" +import type { Lsn, NodeId } from "../atoms.js" + +// --------------------------------------------------------------------------- +// Planes & basis +// --------------------------------------------------------------------------- + +/** + * The four conceptual planes that partition the node space. + * + * Each plane groups node kinds that share a common concern: + * - `intent` what and why + * - `oracle` how we know + * - `design` how it's shaped + * - `plan` how it's sequenced + */ +export type NodePlane = "intent" | "oracle" | "design" | "plan" + +/** + * How a node entered graph truth. + * + * Derived from `db/schema.ts` — same semantics as EdgeBasis. + */ +export type NodeBasis = typeof NODE_BASES[number] + +// --------------------------------------------------------------------------- +// Kind taxonomy — derived from db/schema.ts const arrays +// --------------------------------------------------------------------------- + +/** + * Intent-plane kinds, spanning three derived categories: + * - basic: `goal`, `thesis`, `term`, `context` + * - structural: `requirement`, `assumption`, `constraint`, `invariant` + * - reasoning: `decision`, `criterion`, `example` + */ +export type IntentKind = typeof INTENT_KINDS[number] + +/** Oracle-plane kinds. */ +export type OracleKind = typeof ORACLE_KINDS[number] + +/** Design-plane kinds. */ +export type DesignKind = typeof DESIGN_KINDS[number] + +/** Plan-plane kinds. */ +export type PlanKind = typeof PLAN_KINDS[number] + +/** Union of every node kind across all planes. */ +export type NodeKind = IntentKind | OracleKind | DesignKind | PlanKind + +// --------------------------------------------------------------------------- +// Intent kind categories (derived, not stored) +// --------------------------------------------------------------------------- + +/** + * Derived grouping over {@link IntentKind}. + * + * Never persisted — computed via {@link intentKindCategory}. + */ +export type IntentKindCategory = "basic" | "structural" | "reasoning" + +/** Pure derivation: intent kind → category. */ +export function intentKindCategory(kind: IntentKind): IntentKindCategory { + switch (kind) { + case "goal": + case "thesis": + case "term": + case "context": + return "basic" + case "requirement": + case "assumption": + case "constraint": + case "invariant": + return "structural" + case "decision": + case "criterion": + case "example": + return "reasoning" + } +} + +// --------------------------------------------------------------------------- +// Per-kind detail schemas +// --------------------------------------------------------------------------- + +/** Detail payload for `decision` nodes. */ +export interface DecisionDetail { + readonly chosen_option: string + readonly rejected: readonly string[] + readonly rationale: string +} + +/** Detail payload for `term` nodes. */ +export interface TermDetail { + readonly definition: string + readonly aliases?: readonly string[] +} + +/** Discriminated union of all per-kind detail payloads. */ +export type NodeDetail = DecisionDetail | TermDetail + +// --------------------------------------------------------------------------- +// Main node interface +// --------------------------------------------------------------------------- + +/** + * A typed node in the Brunch graph. + * + * Immutability after acceptance: + * - `plane`, `kind`, `id` are immutable. + * - `title`, `body`, `detail`, `source` may be updated (advances `updatedAtLsn`). + * - To change kind: delete and recreate. + * + * No `status` field: accepted graph nodes are present-or-absent. + * Stale nodes surface as `ReconciliationNeed` records. + */ +export interface GraphNode { + readonly id: NodeId + readonly plane: NodePlane + readonly kind: NodeKind + readonly title: string + readonly body?: string + readonly basis: NodeBasis + readonly source?: string + readonly detail?: NodeDetail + readonly createdAtLsn: Lsn + readonly updatedAtLsn: Lsn +} diff --git a/src/graph/schema/reconciliation-need.ts b/src/graph/schema/reconciliation-need.ts new file mode 100644 index 000000000..a629bf045 --- /dev/null +++ b/src/graph/schema/reconciliation-need.ts @@ -0,0 +1,56 @@ +/** + * Reconciliation-need type definitions. + * + * Canonical reference: docs/design/GRAPH_MODEL.md §"ReconciliationNeed" + * + * A reconciliation_need is a first-class record of an open impasse + * over graph state — typically "this edge needs re-validation" + * (after an upstream change) or "these two nodes might need an edge." + * + * Reconciliation_needs reference graph state. They are NOT graph + * edges and do not appear in projection neighborhoods as edges. + * They surface to the user through next-turn delivery as advisory + * items (D29-L). + * + * Phase 1 lock-and-materialize: type definitions only. + * Drizzle table definitions and CommandExecutor write paths land + * with subsequent M4 slices. + */ + +import type { EdgeId, Lsn, NodeId } from "../atoms.js" + +/** + * What sort of impasse this need records. + * + * Open extension — new kinds may be added as concrete needs surface. + * Most needs are `edge_revalidation`. + */ +export type ReconciliationNeedKind = "edge_revalidation" | "possible_relation" | "possible_duplicate" | "semantic_conflict" + +/** + * What this need is about. + * + * `edge` is the default — the need describes a relation whose + * semantic basis may have changed. + * + * `node_pair` covers cases where no edge exists yet (possible + * duplicate, possible relation). When such a need resolves to + * "yes, edge exists," create the edge and close the need. + */ +export type ReconciliationNeedTarget = { + readonly kind: "edge" + readonly edgeId: EdgeId +} | { + readonly kind: "node_pair" + readonly aId: NodeId + readonly bId: NodeId +} + +export interface ReconciliationNeed { + readonly id: string + readonly kind: ReconciliationNeedKind + readonly target: ReconciliationNeedTarget + readonly rationale?: string + readonly createdAtLsn: Lsn + readonly resolvedAtLsn?: Lsn +} diff --git a/src/graph/snapshot.test.ts b/src/graph/snapshot.test.ts new file mode 100644 index 000000000..d460bca80 --- /dev/null +++ b/src/graph/snapshot.test.ts @@ -0,0 +1,354 @@ +/** + * Graph snapshot reader tests — acceptance criteria for I35-L. + * + * SPEC: D52-L (graph/ reads db/), I35-L (cursory + neighborhood) + * Scope card: Graph snapshot readers at cursory and neighborhood detail levels + * + * All graph state is seeded via CommandExecutor (no direct db writes). + */ + +import { beforeEach, describe, expect, it } from "vitest" + +import { createDb, type BrunchDb } from "../db/connection.js" +import { CommandExecutor } from "./command-executor.js" +import { + getGraphOverview, + getNodeNeighborhood, + getOpenReconciliationNeeds, +} from "./snapshot.js" + +function createTestDb(): BrunchDb { + return createDb(":memory:") +} + +describe("getGraphOverview", () => { + let db: BrunchDb + let executor: CommandExecutor + + beforeEach(() => { + db = createTestDb() + executor = new CommandExecutor(db) + }) + + it("returns empty arrays and zero counts on an empty graph", () => { + const overview = getGraphOverview(db) + expect(overview.nodes).toEqual([]) + expect(overview.edges).toEqual([]) + expect(overview.nodeCount).toBe(0) + expect(overview.edgeCount).toBe(0) + expect(overview.lsn).toBe(0) + }) + + it("returns current LSN from graph_clock", () => { + executor.createNode({ plane: "intent", kind: "goal", title: "G1" }) + executor.createNode({ plane: "intent", kind: "thesis", title: "T1" }) + const overview = getGraphOverview(db) + expect(overview.lsn).toBe(2) + }) + + it("returns typed domain objects with parsed detail JSON", () => { + executor.createNode({ + plane: "intent", + kind: "decision", + title: "Use SQLite", + body: "Settled on SQLite", + detail: { + chosen_option: "SQLite", + rejected: ["PostgreSQL"], + rationale: "Simpler local deployment", + }, + }) + + const overview = getGraphOverview(db) + expect(overview.nodes).toHaveLength(1) + const node = overview.nodes[0]! + expect(node.id).toBeTypeOf("number") + expect(node.plane).toBe("intent") + expect(node.kind).toBe("decision") + expect(node.title).toBe("Use SQLite") + expect(node.body).toBe("Settled on SQLite") + expect(node.basis).toBe("explicit") + expect(node.detail).toEqual({ + chosen_option: "SQLite", + rejected: ["PostgreSQL"], + rationale: "Simpler local deployment", + }) + expect(node.createdAtLsn).toBe(1) + expect(node.updatedAtLsn).toBe(1) + }) + + it("returns nodes and edges with correct counts", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [{ category: "dependency", source: "r1", target: "a1" }], + }) + expect(batch.status).toBe("success") + + const overview = getGraphOverview(db) + expect(overview.nodeCount).toBe(2) + expect(overview.edgeCount).toBe(1) + expect(overview.nodes).toHaveLength(2) + expect(overview.edges).toHaveLength(1) + expect(overview.edges[0]!.category).toBe("dependency") + }) + + it("excludes superseded predecessors from overview", () => { + // Create R_v0, then R_v1 that supersedes R_v0 + const r0 = executor.createNode({ + plane: "intent", + kind: "requirement", + title: "R_offline_v0", + }) + expect(r0.status).toBe("success") + if (r0.status !== "success") throw new Error("unreachable") + const r0Id = r0.nodeId + + const batch = executor.commitGraph({ + nodes: [ + { + ref: "r1", + plane: "intent", + kind: "requirement", + title: "R_offline_v1", + }, + ], + edges: [ + { + category: "supersession", + source: "r1", + target: { existing: r0Id }, + }, + ], + }) + expect(batch.status).toBe("success") + + const overview = getGraphOverview(db) + // R_offline_v0 should be excluded (it is a superseded predecessor) + const titles = overview.nodes.map((n) => n.title) + expect(titles).toContain("R_offline_v1") + expect(titles).not.toContain("R_offline_v0") + // The supersession edge should still be present + expect(overview.edges).toHaveLength(1) + }) +}) + +describe("getNodeNeighborhood", () => { + let db: BrunchDb + let executor: CommandExecutor + + beforeEach(() => { + db = createTestDb() + executor = new CommandExecutor(db) + }) + + it("returns error for non-existent nodeId", () => { + const result = getNodeNeighborhood(db, 999) + expect(result.status).toBe("not_found") + }) + + it("returns anchor node and directly connected nodes/edges at 1 hop (default)", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: "g1", plane: "intent", kind: "goal", title: "G1" }, + ], + edges: [ + { category: "dependency", source: "r1", target: "a1" }, + { category: "support", source: "g1", target: "r1", stance: "for" }, + ], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const r1Id = batch.nodes["r1"]! + const result = getNodeNeighborhood(db, r1Id) + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + + expect(result.anchor.title).toBe("R1") + // Should include A1 (dependency target) and G1 (support source) + expect(result.neighbors).toHaveLength(2) + const neighborTitles = result.neighbors.map((n) => n.title).sort() + expect(neighborTitles).toEqual(["A1", "G1"]) + expect(result.edges).toHaveLength(2) + }) + + it("reaches 2-hop neighbors", () => { + // G1 -> R1 -> A1 (chain of depth 2 from G1) + const batch = executor.commitGraph({ + nodes: [ + { ref: "g1", plane: "intent", kind: "goal", title: "G1" }, + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [ + { category: "support", source: "g1", target: "r1", stance: "for" }, + { category: "dependency", source: "r1", target: "a1" }, + ], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const g1Id = batch.nodes["g1"]! + + // 1 hop: only R1 + const hop1 = getNodeNeighborhood(db, g1Id, { hops: 1 }) + expect(hop1.status).toBe("success") + if (hop1.status !== "success") throw new Error("unreachable") + expect(hop1.neighbors.map((n) => n.title)).toEqual(["R1"]) + + // 2 hops: R1 and A1 + const hop2 = getNodeNeighborhood(db, g1Id, { hops: 2 }) + expect(hop2.status).toBe("success") + if (hop2.status !== "success") throw new Error("unreachable") + const titles = hop2.neighbors.map((n) => n.title).sort() + expect(titles).toEqual(["A1", "R1"]) + }) + + it("excludes superseded predecessors from neighborhood (unless anchor)", () => { + // R_v0 superseded by R_v1, with A1 depending on R_v1 + const r0 = executor.createNode({ + plane: "intent", + kind: "requirement", + title: "R_v0", + }) + expect(r0.status).toBe("success") + if (r0.status !== "success") throw new Error("unreachable") + const r0Id = r0.nodeId + + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R_v1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [ + { category: "supersession", source: "r1", target: { existing: r0Id } }, + { category: "dependency", source: "r1", target: "a1" }, + ], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const r1Id = batch.nodes["r1"]! + + // Neighborhood of R_v1: should include A1 but exclude R_v0 + const result = getNodeNeighborhood(db, r1Id) + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + + const neighborTitles = result.neighbors.map((n) => n.title) + expect(neighborTitles).toContain("A1") + expect(neighborTitles).not.toContain("R_v0") + + // But if R_v0 is the anchor, it should still be returned + const r0Result = getNodeNeighborhood(db, r0Id) + expect(r0Result.status).toBe("success") + if (r0Result.status !== "success") throw new Error("unreachable") + expect(r0Result.anchor.title).toBe("R_v0") + }) + + it("returns typed GraphNode and GraphEdge domain objects", () => { + const batch = executor.commitGraph({ + nodes: [ + { + ref: "t1", + plane: "intent", + kind: "term", + title: "Widget", + detail: { definition: "A reusable component", aliases: ["gadget"] }, + }, + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + ], + edges: [{ category: "boundary", source: "t1", target: "r1" }], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const t1Id = batch.nodes["t1"]! + const result = getNodeNeighborhood(db, t1Id) + expect(result.status).toBe("success") + if (result.status !== "success") throw new Error("unreachable") + + // Anchor has parsed detail + expect(result.anchor.detail).toEqual({ + definition: "A reusable component", + aliases: ["gadget"], + }) + + // Edge has typed fields + const edge = result.edges[0]! + expect(edge.category).toBe("boundary") + expect(edge.sourceId).toBe(t1Id) + expect(edge.createdAtLsn).toBeTypeOf("number") + }) +}) + +describe("getOpenReconciliationNeeds", () => { + let db: BrunchDb + let executor: CommandExecutor + + beforeEach(() => { + db = createTestDb() + executor = new CommandExecutor(db) + }) + + it("returns empty array when no needs exist", () => { + const needs = getOpenReconciliationNeeds(db) + expect(needs).toEqual([]) + }) + + it("returns open needs as typed domain objects", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [{ category: "dependency", source: "r1", target: "a1" }], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const create = executor.createReconciliationNeed({ + target: { kind: "edge", edgeId: batch.edges[0]! }, + needKind: "edge_revalidation", + reason: "upstream changed", + }) + expect(create.status).toBe("success") + if (create.status !== "success") throw new Error("unreachable") + + const needs = getOpenReconciliationNeeds(db) + expect(needs).toHaveLength(1) + expect(needs[0]!.kind).toBe("edge_revalidation") + expect(needs[0]!.target).toEqual({ kind: "edge", edgeId: batch.edges[0]! }) + expect(needs[0]!.rationale).toBe("upstream changed") + expect(needs[0]!.createdAtLsn).toBeTypeOf("number") + }) + + it("excludes resolved needs", () => { + const batch = executor.commitGraph({ + nodes: [ + { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + ], + edges: [{ category: "dependency", source: "r1", target: "a1" }], + }) + expect(batch.status).toBe("success") + if (batch.status !== "success") throw new Error("unreachable") + + const create = executor.createReconciliationNeed({ + target: { kind: "edge", edgeId: batch.edges[0]! }, + needKind: "edge_revalidation", + }) + expect(create.status).toBe("success") + if (create.status !== "success") throw new Error("unreachable") + + executor.resolveReconciliationNeed(create.id) + + const needs = getOpenReconciliationNeeds(db) + expect(needs).toEqual([]) + }) +}) diff --git a/src/graph/snapshot.ts b/src/graph/snapshot.ts new file mode 100644 index 000000000..0b41ad11a --- /dev/null +++ b/src/graph/snapshot.ts @@ -0,0 +1,285 @@ +/** + * Graph snapshot readers — cursory overview and node neighborhood. + * + * SPEC: I35-L (two detail levels), D52-L (graph/ reads db/) + * + * These are pure read functions over BrunchDb. They return typed + * domain objects (GraphNode, GraphEdge), not raw Drizzle rows. + * Superseded predecessors (nodes that are targets of a `supersession` + * edge) are excluded per CATEGORY_POLICY projectionEffect. + */ + +import { eq, or, inArray } from "drizzle-orm" + +import type { BrunchDb } from "../db/connection.js" +import * as schema from "../db/schema.js" +import type { GraphEdge } from "./schema/edges.js" +import type { GraphNode, NodeDetail } from "./schema/nodes.js" +import type { + ReconciliationNeed, + ReconciliationNeedTarget, +} from "./schema/reconciliation-need.js" + +// --------------------------------------------------------------------------- +// Return types +// --------------------------------------------------------------------------- + +/** Full-graph cursory overview. */ +export interface GraphOverview { + readonly nodes: readonly GraphNode[] + readonly edges: readonly GraphEdge[] + readonly nodeCount: number + readonly edgeCount: number + /** Current LSN from graph_clock. */ + readonly lsn: number +} + +/** Successful neighborhood result. */ +export interface NeighborhoodSuccess { + readonly status: "success" + readonly anchor: GraphNode + readonly neighbors: readonly GraphNode[] + readonly edges: readonly GraphEdge[] +} + +/** Node not found. */ +export interface NeighborhoodNotFound { + readonly status: "not_found" +} + +export type NeighborhoodResult = NeighborhoodSuccess | NeighborhoodNotFound + +export interface NeighborhoodOptions { + /** Number of hops from the anchor node. Defaults to 1. */ + readonly hops?: number +} + +// --------------------------------------------------------------------------- +// Row → domain mapping +// --------------------------------------------------------------------------- + +function rowToNode(row: typeof schema.nodes.$inferSelect): GraphNode { + return { + id: row.id, + plane: row.plane as GraphNode["plane"], + kind: row.kind as GraphNode["kind"], + title: row.title, + ...(row.body != null ? { body: row.body } : {}), + basis: row.basis as GraphNode["basis"], + ...(row.source != null ? { source: row.source } : {}), + ...(row.detail != null + ? { detail: JSON.parse(row.detail) as NodeDetail } + : {}), + createdAtLsn: row.created_at_lsn, + updatedAtLsn: row.updated_at_lsn, + } +} + +function rowToEdge(row: typeof schema.edges.$inferSelect): GraphEdge { + const base = { + id: row.id, + category: row.category as GraphEdge["category"], + sourceId: row.source_id, + targetId: row.target_id, + basis: row.basis as GraphEdge["basis"], + createdAtLsn: row.created_at_lsn, + updatedAtLsn: row.updated_at_lsn, + } + return row.stance != null + ? row.rationale != null + ? { + ...base, + stance: row.stance as NonNullable, + rationale: row.rationale, + } + : { ...base, stance: row.stance as NonNullable } + : row.rationale != null + ? { ...base, rationale: row.rationale } + : base +} + +// --------------------------------------------------------------------------- +// Supersession helpers +// --------------------------------------------------------------------------- + +/** Return the set of node ids that are superseded predecessors. */ +function getSupersededIds(db: BrunchDb): Set { + const rows = db + .select({ targetId: schema.edges.target_id }) + .from(schema.edges) + .where(eq(schema.edges.category, "supersession")) + .all() + return new Set(rows.map((r) => r.targetId)) +} + +// --------------------------------------------------------------------------- +// getGraphOverview +// --------------------------------------------------------------------------- + +/** + * Cursory full-graph overview. + * + * Returns all accepted nodes and edges with current LSN. + * Superseded predecessors are excluded from the node list + * per CATEGORY_POLICY.supersession.projectionEffect. + */ +export function getGraphOverview(db: BrunchDb): GraphOverview { + const supersededIds = getSupersededIds(db) + + const allNodeRows = db.select().from(schema.nodes).all() + const allEdgeRows = db.select().from(schema.edges).all() + + const nodes = allNodeRows + .filter((r) => !supersededIds.has(r.id)) + .map(rowToNode) + + const edges = allEdgeRows.map(rowToEdge) + + const clockRow = db.select().from(schema.graphClock).get() + const lsn = clockRow?.lsn ?? 0 + + return { + nodes, + edges, + nodeCount: nodes.length, + edgeCount: edges.length, + lsn, + } +} + +// --------------------------------------------------------------------------- +// getNodeNeighborhood +// --------------------------------------------------------------------------- + +/** + * Neighborhood snapshot around a given node. + * + * Returns the anchor node, all reachable neighbors within `hops` + * distance (default 1), and the edges connecting them. + * Superseded predecessors are excluded from neighbors + * (unless the predecessor is the anchor itself). + */ +export function getNodeNeighborhood( + db: BrunchDb, + nodeId: number, + options?: NeighborhoodOptions, +): NeighborhoodResult { + const hops = options?.hops ?? 1 + + // Verify anchor exists + const anchorRow = db + .select() + .from(schema.nodes) + .where(eq(schema.nodes.id, nodeId)) + .get() + + if (!anchorRow) { + return { status: "not_found" } + } + + const supersededIds = getSupersededIds(db) + const anchor = rowToNode(anchorRow) + + // BFS traversal: collect reachable node ids within hop distance + const visited = new Set([nodeId]) + let frontier = new Set([nodeId]) + const collectedEdgeIds = new Set() + + for (let hop = 0; hop < hops; hop++) { + if (frontier.size === 0) break + + // Find all edges touching frontier nodes + const frontierArr = [...frontier] + const edgeRows = db + .select() + .from(schema.edges) + .where( + or( + inArray(schema.edges.source_id, frontierArr), + inArray(schema.edges.target_id, frontierArr), + ), + ) + .all() + + const nextFrontier = new Set() + for (const edge of edgeRows) { + collectedEdgeIds.add(edge.id) + for (const peerId of [edge.source_id, edge.target_id]) { + if (!visited.has(peerId)) { + // Exclude superseded predecessors (unless it's the anchor) + if (supersededIds.has(peerId) && peerId !== nodeId) continue + visited.add(peerId) + nextFrontier.add(peerId) + } + } + } + frontier = nextFrontier + } + + // Fetch neighbor nodes (exclude anchor) + const neighborIds = [...visited].filter((id) => id !== nodeId) + const neighborNodes: GraphNode[] = [] + if (neighborIds.length > 0) { + const rows = db + .select() + .from(schema.nodes) + .where(inArray(schema.nodes.id, neighborIds)) + .all() + neighborNodes.push(...rows.map(rowToNode)) + } + + // Fetch collected edges + const edgeIdArr = [...collectedEdgeIds] + const edgeNodes: GraphEdge[] = [] + if (edgeIdArr.length > 0) { + const rows = db + .select() + .from(schema.edges) + .where(inArray(schema.edges.id, edgeIdArr)) + .all() + edgeNodes.push(...rows.map(rowToEdge)) + } + + return { + status: "success", + anchor, + neighbors: neighborNodes, + edges: edgeNodes, + } +} + +// --------------------------------------------------------------------------- +// getOpenReconciliationNeeds +// --------------------------------------------------------------------------- + +function rowToReconNeed( + row: typeof schema.reconciliationNeed.$inferSelect, +): ReconciliationNeed { + const target: ReconciliationNeedTarget = + row.target_kind === "edge" + ? { kind: "edge", edgeId: row.target_edge_id! } + : { kind: "node_pair", aId: row.target_a_id!, bId: row.target_b_id! } + + return { + id: String(row.id), + kind: row.kind as ReconciliationNeed["kind"], + target, + ...(row.reason != null ? { rationale: row.reason } : {}), + createdAtLsn: row.created_at_lsn, + ...(row.resolved_at_lsn != null + ? { resolvedAtLsn: row.resolved_at_lsn } + : {}), + } +} + +/** + * Return all open (unresolved) reconciliation needs. + */ +export function getOpenReconciliationNeeds(db: BrunchDb): ReconciliationNeed[] { + const rows = db + .select() + .from(schema.reconciliationNeed) + .where(eq(schema.reconciliationNeed.status, "open")) + .all() + return rows.map(rowToReconNeed) +} diff --git a/src/session/README.md b/src/session/README.md new file mode 100644 index 000000000..55af3f8da --- /dev/null +++ b/src/session/README.md @@ -0,0 +1,56 @@ +# session/ — Session domain layer + +SPEC decisions: D6-L, D11-L, D12-L, D13-L, D21-L, D52-L + +## Owns + +Projection of Brunch's session semantics out of Pi's JSONL substrate, +plus the coordination logic for workspace/spec/session lifecycle. + +- **Transcript projection** — reading Pi JSONL, projecting Brunch-relevant + structure (assistant/user rows, custom entries, tool results). + +- **Exchange extraction** — elicitation exchange projection: prompt-side + span + response-side span, per D13-L. + +- **Workspace coordination** — boot flow, spec/session selection, + `.brunch/state.json` management. The `WorkspaceSessionCoordinator` + is the only module that creates/opens Pi sessions for Brunch user flows + and writes `brunch.session_binding`. + +- **Session binding** — session↔spec binding entries in JSONL. + +- **Session envelope** — canonical session envelope reader (spec/session pair). + +- **LSN staleness tracking** — Pi extension records current LSN at session + start, checks at `prepareNextTurn`, injects `worldUpdate` with optional + re-snapshot when stale. + +## Does NOT own + +- Graph state, CommandExecutor, graph snapshots — those live in `graph/`. +- Prompt composition, context building — those live in `agents/`. +- Pi extension registration — those live in `.pi/extensions/`. + +## Imported by + +- `agents/contexts/` — for session/transcript snapshots +- `rpc/` — for session.* and workspace.* RPC handlers +- `.pi/extensions/` — for session lifecycle hooks + +## Migration from src/ root + +These files currently at `src/` root migrate here incrementally: + +| Current file | Session concern | +|-----------------------------------|------------------------------------| +| `workspace-session-coordinator.ts`| boot, spec/session selection | +| `session-binding.ts` | session↔spec binding | +| `brunch-session-envelope.ts` | session envelope reader | +| `session-projection-reader.ts` | JSONL projection | +| `session-transcript.ts` | transcript row projection | +| `elicitation-exchange.ts` | exchange extraction | +| `structured-exchange.ts` | structured exchange schemas/types | + +Move each file when it is next touched for substantive work, not as a +bulk rename. Update imports at the call sites. diff --git a/src/tui-client/.pi/__tests__/chrome.test.ts b/src/tui-client/.pi/__tests__/chrome.test.ts index 40cdbefc3..a99f43574 100644 --- a/src/tui-client/.pi/__tests__/chrome.test.ts +++ b/src/tui-client/.pi/__tests__/chrome.test.ts @@ -22,6 +22,20 @@ describe("Brunch chrome projection", () => { ) }) + it("populates session.label from workspace session name when available", () => { + const workspace = readyWorkspace( + "/tmp/project", + "session-abc", + "My spec — session 1", + ) + const state = chromeStateForWorkspace(workspace) + + expect(state.session.label).toBe("My spec — session 1") + expect(formatBrunchChromeHeaderLines(state).join("\n")).toContain( + "My spec — session 1", + ) + }) + it("formats chrome header as wordmark plus runtime-state summary", async () => { const state = { cwd: "/tmp/project", @@ -204,6 +218,7 @@ describe("Brunch chrome projection", () => { function readyWorkspace( cwd: string, sessionId: string, + sessionName?: string, ): WorkspaceSessionReadyState { const spec = { id: "spec-1", title: "Spec One" } return { @@ -213,6 +228,7 @@ function readyWorkspace( session: { id: sessionId, file: `/sessions/${sessionId}.jsonl`, + name: sessionName, manager: {} as WorkspaceSessionReadyState["session"]["manager"], }, chrome: { diff --git a/src/tui-client/.pi/__tests__/operational-mode.test.ts b/src/tui-client/.pi/__tests__/operational-mode.test.ts index 6ae731124..11f86ce14 100644 --- a/src/tui-client/.pi/__tests__/operational-mode.test.ts +++ b/src/tui-client/.pi/__tests__/operational-mode.test.ts @@ -223,18 +223,85 @@ describe("Brunch agent runtime-state projection", () => { it("rejects invalid runtime switch combinations before appending", () => { const manager = new FakeRuntimeStateSessionManager() - expect(() => - appendBrunchAgentRuntimeSwitch(manager, { + for (const invalidState of [ + { + schemaVersion: 1, + operationalMode: "execute", + agentRole: "elicitor", + agentStrategy: "step-by-step", + agentLens: "step-by-step", + }, + { + schemaVersion: 1, + operationalMode: "elicit", + agentRole: "reviewer", + agentStrategy: "step-by-step", + agentLens: "step-by-step", + }, + { schemaVersion: 1, operationalMode: "elicit", agentRole: "elicitor", agentStrategy: "not-a-strategy", agentLens: "step-by-step", - } as unknown as BrunchAgentState), - ).toThrow("Invalid BrunchAgentState runtime selection.") + }, + { + schemaVersion: 1, + operationalMode: "elicit", + agentRole: "elicitor", + agentStrategy: "step-by-step", + agentLens: "not-a-lens", + }, + ]) { + expect(() => + appendBrunchAgentRuntimeSwitch( + manager, + invalidState as unknown as BrunchAgentState, + ), + ).toThrow("Invalid BrunchAgentState runtime selection.") + } expect(manager.entries).toEqual([]) }) + it("does not project invalid runtime mode, role, strategy, or lens entries", () => { + for (const invalidState of [ + { + schemaVersion: 1, + operationalMode: "execute", + agentRole: "elicitor", + agentStrategy: "step-by-step", + agentLens: "step-by-step", + }, + { + schemaVersion: 1, + operationalMode: "elicit", + agentRole: "reviewer", + agentStrategy: "step-by-step", + agentLens: "step-by-step", + }, + { + schemaVersion: 1, + operationalMode: "elicit", + agentRole: "elicitor", + agentStrategy: "not-a-strategy", + agentLens: "step-by-step", + }, + { + schemaVersion: 1, + operationalMode: "elicit", + agentRole: "elicitor", + agentStrategy: "step-by-step", + agentLens: "not-a-lens", + }, + ]) { + expect( + projectBrunchAgentState([ + runtimeEntry(invalidState as unknown as BrunchAgentState), + ]), + ).toMatchObject(DEFAULT_BRUNCH_AGENT_STATE) + } + }) + it("appends runtime init from the extension session-start hook", async () => { const manager = new FakeRuntimeStateSessionManager() const events: Record unknown> = {} diff --git a/src/tui-client/.pi/__tests__/prompting.test.ts b/src/tui-client/.pi/__tests__/prompting.test.ts index 59f3ad549..4fd6f7555 100644 --- a/src/tui-client/.pi/__tests__/prompting.test.ts +++ b/src/tui-client/.pi/__tests__/prompting.test.ts @@ -6,8 +6,12 @@ import { describe, expect, it } from "vitest" import { composeBrunchPrompt } from "../context/compose-brunch-prompt.js" import { + BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, DEFAULT_BRUNCH_AGENT_STATE, + appendBrunchAgentRuntimeSwitch, type BrunchAgentState, + type BrunchAgentStateEntryData, + registerBrunchOperationalModePolicy, } from "../extensions/operational-mode.js" import { registerBrunchPrompting } from "../extensions/prompting.js" import { createBrunchPiExtensionShell } from "../../pi-extension-shell.js" @@ -25,6 +29,23 @@ function runtimeEntry(state: BrunchAgentState) { } } +class FakeRuntimeStateSessionManager { + entries: Array<{ + type: "custom" + customType: string + data: BrunchAgentStateEntryData + }> = [] + + getEntries() { + return this.entries + } + + appendCustomEntry(customType: string, data: BrunchAgentStateEntryData) { + this.entries.push({ type: "custom", customType, data }) + return `entry-${this.entries.length}` + } +} + describe("Brunch prompt-pack topology", () => { it("composes deterministic private prompt packs in stable order", () => { const result = composeBrunchPrompt({ @@ -108,6 +129,77 @@ describe("Brunch prompt-pack topology", () => { }) }) + it("derives prompt and active tools from the same transcript-backed runtime state", async () => { + const manager = new FakeRuntimeStateSessionManager() + const events: Record unknown>> = {} + const activeTools: string[][] = [] + + const pi = { + on: (event: string, handler: (event: never, ctx?: never) => unknown) => { + events[event] ??= [] + events[event].push(handler) + }, + registerTool: (_tool: { name: string }) => {}, + getAllTools: () => + ["read", "grep", "bash", "edit", "write", "present_options"].map( + (name) => ({ name }), + ), + setActiveTools: (tools: string[]) => activeTools.push(tools), + } + registerBrunchOperationalModePolicy(pi as never) + registerBrunchPrompting(pi as never) + + for (const handler of events.session_start ?? []) { + await handler({} as never, { sessionManager: manager } as never) + } + const defaultPromptResults = await Promise.all( + (events.before_agent_start ?? []).map((handler) => + Promise.resolve( + handler({ systemPrompt: "base" } as never, { + sessionManager: manager, + } as never), + ), + ), + ) + const latestState: BrunchAgentState = { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentStrategy: "disambiguate-via-examples", + agentLens: "disambiguate-via-examples", + } + appendBrunchAgentRuntimeSwitch(manager, latestState, "user") + const switchedPromptResults = await Promise.all( + (events.before_agent_start ?? []).map((handler) => + Promise.resolve( + handler({ systemPrompt: "base" } as never, { + sessionManager: manager, + } as never), + ), + ), + ) + const defaultPrompt = defaultPromptResults.find(Boolean) + const switchedPrompt = switchedPromptResults.find(Boolean) + + expect(manager.entries[0]?.customType).toBe( + BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, + ) + expect(activeTools).toEqual([ + ["read", "grep", "present_options"], + ["read", "grep", "present_options"], + ["read", "grep", "present_options"], + ]) + expect(defaultPrompt).toMatchObject({ + systemPrompt: expect.stringContaining("Agent strategy: step-by-step."), + }) + expect(switchedPrompt).toMatchObject({ + systemPrompt: expect.stringContaining( + "Agent strategy: disambiguate-via-examples.", + ), + }) + }) + it("is registered by the explicit shell after operational-mode policy", async () => { const eventNames: string[] = [] diff --git a/src/tui-client/.pi/extensions/chrome.ts b/src/tui-client/.pi/extensions/chrome.ts index 7764dd7b7..e0ed6a98b 100644 --- a/src/tui-client/.pi/extensions/chrome.ts +++ b/src/tui-client/.pi/extensions/chrome.ts @@ -132,7 +132,7 @@ export function chromeStateForWorkspace( ...workspace.chrome, session: { id: workspace.session.id, - label: workspace.session.id, + label: workspace.session.name ?? workspace.session.id, }, } } diff --git a/src/workspace-session-coordinator.test.ts b/src/workspace-session-coordinator.test.ts index 81b864fd7..cdf40239d 100644 --- a/src/workspace-session-coordinator.test.ts +++ b/src/workspace-session-coordinator.test.ts @@ -565,4 +565,43 @@ describe("WorkspaceSessionCoordinator", () => { expect(result.chrome.cwd).toBe(cwd) expect(result.chrome.spec).toBeNull() }) + + it("generates a display name for new sessions and persists it as session_info", async () => { + const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) + const coordinator = createWorkspaceSessionCoordinator({ cwd }) + + const first = await coordinator.createSetupSession({ + specTitle: "Scratch spec", + }) + + // Session should have a display name derived from spec title + const manager1 = SessionManager.open(first.session.file, undefined, cwd) + expect(manager1.getSessionName()).toBe("Scratch spec — session 1") + + // Second session for same spec gets ordinal 2 + const second = await coordinator.createSetupSessionForCurrentSpec() + expect(second.status).toBe("ready") + if (second.status !== "ready") return + + const manager2 = SessionManager.open(second.session.file, undefined, cwd) + expect(manager2.getSessionName()).toBe("Scratch spec — session 2") + }) + + it("preserves existing display name on session resume", async () => { + const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) + const coordinator = createWorkspaceSessionCoordinator({ cwd }) + + await coordinator.createSetupSession({ + specTitle: "My spec", + }) + + // Reopen the same session + const reopened = await coordinator.openDefaultWorkspace() + expect(reopened.status).toBe("ready") + if (reopened.status !== "ready") return + + // Name should be unchanged + const manager = SessionManager.open(reopened.session.file, undefined, cwd) + expect(manager.getSessionName()).toBe("My spec — session 1") + }) }) diff --git a/src/workspace-session-coordinator.ts b/src/workspace-session-coordinator.ts index 9dd0ea8bc..1c8a544b7 100644 --- a/src/workspace-session-coordinator.ts +++ b/src/workspace-session-coordinator.ts @@ -44,6 +44,7 @@ export interface WorkspaceSessionReadyState { session: { id: string file: string + name?: string manager: SessionManager } chrome: WorkspaceSessionChromeState @@ -322,12 +323,28 @@ async function createBoundSession( spec: WorkspaceSpecState, ): Promise { await ensureWorkspaceDirs(cwd) + const existingSessionCount = await countSessionsForSpec(cwd, spec.id) const manager = SessionManager.create(cwd, sessionDir(cwd)) const sessionFile = manager.getSessionFile() if (!sessionFile) { throw new Error("Pi SessionManager did not create a persisted session file") } - return bindSessionToSpec(manager, spec) + return bindSessionToSpec(manager, spec, existingSessionCount + 1) +} + +async function countSessionsForSpec( + cwd: string, + specId: string, +): Promise { + const files = await listSessionFiles(cwd) + let count = 0 + for (const file of files) { + const session = await inspectSessionFile(file) + if (session.available && session.specId === specId) { + count++ + } + } + return count } async function openCurrentSession( @@ -352,6 +369,7 @@ async function openCurrentSession( function bindSessionToSpec( manager: SessionManager, spec: WorkspaceSpecState, + sessionOrdinal?: number, ): WorkspaceSessionReadyState["session"] { const sessionFile = manager.getSessionFile() if (!sessionFile) { @@ -368,6 +386,11 @@ function bindSessionToSpec( specTitle: spec.title, }), ) + // Generate and persist a display name for new sessions + if (sessionOrdinal !== undefined) { + const displayName = sessionDisplayName(spec.title, sessionOrdinal) + manager.appendSessionInfo(displayName) + } } else if ( existingBindings.length !== 1 || existingBindings[0]?.data.sessionId !== manager.getSessionId() || @@ -379,7 +402,17 @@ function bindSessionToSpec( } flushSessionWithoutAssistant(manager) - return { id: manager.getSessionId(), file: sessionFile, manager } + const sessionName = manager.getSessionName() + return { + id: manager.getSessionId(), + file: sessionFile, + ...(sessionName != null ? { name: sessionName } : {}), + manager, + } +} + +export function sessionDisplayName(specTitle: string, ordinal: number): string { + return `${specTitle} — session ${ordinal}` } interface FlushableSessionManager { @@ -501,11 +534,19 @@ async function inspectSessionFile(file: string): Promise { return { file, reason: "incompatible_binding", available: false } } + const sessionInfoEntries = entries.filter(isSessionInfoEntry) + const lastInfo = + sessionInfoEntries.length > 0 + ? sessionInfoEntries[sessionInfoEntries.length - 1] as { name?: string } + : undefined + const name = lastInfo?.name + return { id: header.id, file, specId: binding.data.specId, specTitle: binding.data.specTitle, + ...(name != null ? { name } : {}), available: true, } } @@ -696,3 +737,11 @@ function isSessionHeader(value: unknown): value is SessionHeader { typeof (value as { id?: unknown }).id === "string" ) } + +function isSessionInfoEntry(value: unknown): boolean { + return ( + typeof value === "object" && + value !== null && + (value as { type?: unknown }).type === "session_info" + ) +}