diff --git a/.agents/skills/ln-scope/SKILL.md b/.agents/skills/ln-scope/SKILL.md index 0c2ead83b..ebbfd89f5 100644 --- a/.agents/skills/ln-scope/SKILL.md +++ b/.agents/skills/ln-scope/SKILL.md @@ -96,7 +96,7 @@ Chain discipline: - keep chains short — typically 2–5 cards - keep each card in full or light scope-card format -- mark card status clearly (`next`, `in progress`, `done`, `dropped`) +- mark card status clearly (`next`, `in progress`, `done`, `dropped`, `stale`) - if any card trips the promotion checklist, reveals a frontier split, or turns out to depend on unknown results from an earlier card, stop the chain and route back through `ln-spec` or `ln-plan` as appropriate - delete the scope file when its chain is exhausted or superseded (per-file deletion only) diff --git a/.fixtures/README.md b/.fixtures/README.md index 6bb262800..a30d47746 100644 --- a/.fixtures/README.md +++ b/.fixtures/README.md @@ -1,7 +1,6 @@ # `.fixtures/` -Current probe artifacts and transcript evidence for the Brunch POC. - +Current seed data plus probe artifacts and transcript evidence for the Brunch POC. The active convention is **probe first, transcript-backed**: each committed run must have a probe id, a run id, executable/reportable oracle output, and the transcript artifact needed for human review. Brief-based golden fixtures may @@ -15,15 +14,24 @@ for the current architecture. ``` .fixtures/ +├── seeds/ +│ └── / +│ ├── README.md +│ ├── .json # Reusable explicit-basis starting truth +│ └── _*.ts # Reproducible data-prep scripts, not product code └── runs/ └── / └── / ├── session.jsonl # Source transcript / canonical run evidence ├── transcript.md # Human-readable semantic rendering - └── report.json # Probe report and artifact paths + ├── report.json # Probe report and artifact paths + └── graph-snapshot.json # Optional graph readback when graph truth is the proof target ``` ## Current runs - `runs/public-rpc-parity/2026-05-29-public-rpc-parity/` — FE-744 public Brunch JSON-RPC structured-exchange parity proof. +- `runs/fixture-curation/fixture-curation-2026-06-05T104440Z/` — + dev-seed-fixtures tracer proving a Bilal-derived explicit base seed can be expanded + through the real `propose-graph`/`commit_graph` product path with implicit graph readback. diff --git a/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/graph-snapshot.json b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/graph-snapshot.json new file mode 100644 index 000000000..9db294dc2 --- /dev/null +++ b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/graph-snapshot.json @@ -0,0 +1,1099 @@ +{ + "nodes": [ + { + "id": 6, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 1, + "title": "The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…", + "body": "The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded corners, or blue primary buttons.", + "basis": "explicit", + "source": "external-observed [C6]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 8, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 2, + "title": "This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…", + "body": "This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitly out of scope.", + "basis": "explicit", + "source": "stakeholder [C5]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 11, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 3, + "title": "The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.", + "body": "The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.", + "basis": "explicit", + "source": "stakeholder [C10]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 13, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 4, + "title": "Users must manually refresh to see new derivation steps in the macro view.", + "body": "Users must manually refresh to see new derivation steps in the macro view.", + "basis": "explicit", + "source": "stakeholder [C12]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 19, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 5, + "title": "The macro view is built inside a Vite + React + Tailwind SPA.", + "body": "The macro view is built inside a Vite + React + Tailwind SPA.", + "basis": "explicit", + "source": "stakeholder [C2]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 21, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 6, + "title": "Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.", + "body": "Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.", + "basis": "explicit", + "source": "stakeholder [C9]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 28, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 7, + "title": "Phase color values must be expressed as oklch values within the phosphor palette.", + "body": "Phase color values must be expressed as oklch values within the phosphor palette.", + "basis": "explicit", + "source": "stakeholder [C13]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 38, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 8, + "title": "The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…", + "body": "The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes and vertical flow.", + "basis": "explicit", + "source": "stakeholder [C3]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 50, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 9, + "title": "The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…", + "body": "The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no generic UI patterns.", + "basis": "explicit", + "source": "stakeholder [C4]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 56, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 10, + "title": "Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.", + "body": "Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.", + "basis": "explicit", + "source": "stakeholder [C8]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 64, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 11, + "title": "The macro view must use React Flow (@xyflow/react) version ^12.", + "body": "The macro view must use React Flow (@xyflow/react) version ^12.", + "basis": "explicit", + "source": "stakeholder [C1]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 70, + "specId": 1, + "plane": "intent", + "kind": "constraint", + "kindOrdinal": 12, + "title": "The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.", + "body": "The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.", + "basis": "explicit", + "source": "stakeholder [C11]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 1, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 1, + "title": "In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…", + "body": "In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a separate LLM agent that synthesizes the minimal viable set before reconciliation.", + "basis": "explicit", + "source": "external-observed [X7]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 2, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 2, + "title": "The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…", + "body": "The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosphor glow behavior, scanline texture, and JetBrains Mono font.", + "basis": "explicit", + "source": "external-observed [X6]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 4, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 3, + "title": "Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…", + "body": "Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor glow, scanline textures).", + "basis": "explicit", + "source": "stakeholder [X11]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 5, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 4, + "title": "Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.", + "body": "Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.", + "basis": "explicit", + "source": "stakeholder [X29]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 10, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 5, + "title": "Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.", + "body": "Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.", + "basis": "explicit", + "source": "stakeholder [X36]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 14, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 6, + "title": "Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.", + "body": "Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.", + "basis": "explicit", + "source": "stakeholder [X31]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 15, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 7, + "title": "The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.", + "body": "The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.", + "basis": "explicit", + "source": "stakeholder [X2]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 16, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 8, + "title": "src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…", + "body": "src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping (amber), --color-phase-pinning (cyan), --color-phase-defining-done (purple), plus --color-phosphor-red, surface-0..3 warm darks, and --font-mono JetBrains Mono.", + "basis": "explicit", + "source": "technical-observed [X39]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 17, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 9, + "title": "Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.", + "body": "Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.", + "basis": "explicit", + "source": "stakeholder [X33]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 18, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 10, + "title": "Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.", + "body": "Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.", + "basis": "explicit", + "source": "stakeholder [X18]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 20, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 11, + "title": "Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…", + "body": "Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the outcome indicator.", + "basis": "explicit", + "source": "stakeholder [X35]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 26, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 12, + "title": "Stakeholder preference: a failed run is shown with a red color and a dimmed interior.", + "body": "Stakeholder preference: a failed run is shown with a red color and a dimmed interior.", + "basis": "explicit", + "source": "stakeholder [X27]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 27, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 13, + "title": "Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…", + "body": "Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information density is high but not overwhelming.", + "basis": "explicit", + "source": "stakeholder [X37]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 29, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 14, + "title": "A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…", + "body": "A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story IR → Layout → Render with React Flow custom node types; the file is otherwise a stub.", + "basis": "explicit", + "source": "technical-observed [X38]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 30, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 15, + "title": "The macro view is one specific view within the broader Spec Explorer UI.", + "body": "The macro view is one specific view within the broader Spec Explorer UI.", + "basis": "explicit", + "source": "stakeholder [X1]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 31, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 16, + "title": "Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…", + "body": "Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue border, bail=red border + dimmed interior.", + "basis": "explicit", + "source": "stakeholder [X28]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 32, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 17, + "title": "Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…", + "body": "Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/commitment), defining_done=violet (completion/closure).", + "basis": "explicit", + "source": "stakeholder [X34]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 33, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 18, + "title": "Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.", + "body": "Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.", + "basis": "explicit", + "source": "stakeholder [X26]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 34, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 19, + "title": "Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…", + "body": "Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constructs.", + "basis": "explicit", + "source": "stakeholder [X15]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 35, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 20, + "title": "The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…", + "body": "The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than graph content.", + "basis": "explicit", + "source": "stakeholder [X3]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 36, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 21, + "title": "Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…", + "body": "Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relationship using lineage edge types.", + "basis": "explicit", + "source": "external-observed [X8]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 37, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 22, + "title": "Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.", + "body": "Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.", + "basis": "explicit", + "source": "stakeholder [X30]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 39, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 23, + "title": "Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…", + "body": "Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation branch.", + "basis": "explicit", + "source": "stakeholder [X16]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 40, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 24, + "title": "Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.", + "body": "Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.", + "basis": "explicit", + "source": "stakeholder [X12]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 41, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 25, + "title": "Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…", + "body": "Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected', red for 'impasse_surfaced' — distinguished by a colored left border or chip.", + "basis": "explicit", + "source": "stakeholder [X32]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 42, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 26, + "title": "Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.", + "body": "Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.", + "basis": "explicit", + "source": "stakeholder [X9]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 44, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 27, + "title": "Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…", + "body": "Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-peel derivation depth.", + "basis": "explicit", + "source": "stakeholder [X14]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 45, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 28, + "title": "The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…", + "body": "The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green and pinning=amber. Reconciling this is a minor concern but theme.css is the source of truth for the existing design system.", + "basis": "explicit", + "source": "technical-observed [X41]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 46, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 29, + "title": "The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…", + "body": "The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md document.", + "basis": "explicit", + "source": "external-observed [X5]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 47, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 30, + "title": "Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.", + "body": "Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.", + "basis": "explicit", + "source": "stakeholder [X24]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 49, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 31, + "title": "Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.", + "body": "Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.", + "basis": "explicit", + "source": "stakeholder [X10]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 51, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 32, + "title": "Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.", + "body": "Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.", + "basis": "explicit", + "source": "stakeholder [X22]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 52, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 33, + "title": "Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.", + "body": "Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.", + "basis": "explicit", + "source": "stakeholder [X17]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 54, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 34, + "title": "Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…", + "body": "Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could be materialized later.", + "basis": "explicit", + "source": "stakeholder [X23]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 55, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 35, + "title": "Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.", + "body": "Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.", + "basis": "explicit", + "source": "stakeholder [X19]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 59, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 36, + "title": "Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…", + "body": "Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING' or '⚡ NUDGE' in phosphor amber) rather than as an external indicator.", + "basis": "explicit", + "source": "stakeholder [X25]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 61, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 37, + "title": "A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…", + "body": "A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasible.", + "basis": "explicit", + "source": "technical-observed [X40]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 65, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 38, + "title": "Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…", + "body": "Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with mode differences expressed via color, label, or border style only.", + "basis": "explicit", + "source": "stakeholder [X20]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 67, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 39, + "title": "A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.", + "body": "A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.", + "basis": "explicit", + "source": "stakeholder [X4]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 68, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 40, + "title": "Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…", + "body": "Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, outcome).", + "basis": "explicit", + "source": "stakeholder [X21]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 69, + "specId": 1, + "plane": "intent", + "kind": "context", + "kindOrdinal": 41, + "title": "Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…", + "body": "Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in, reconciliation, perspective, phantom, and dead-end impasse.", + "basis": "explicit", + "source": "stakeholder [X13]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 43, + "specId": 1, + "plane": "intent", + "kind": "goal", + "kindOrdinal": 1, + "title": "Each node's form and function must visually communicate what happened at that specific point in the derivation tree.", + "body": "Each node's form and function must visually communicate what happened at that specific point in the derivation tree.", + "basis": "explicit", + "source": "stakeholder [G4]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 57, + "specId": 1, + "plane": "intent", + "kind": "goal", + "kindOrdinal": 2, + "title": "The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.", + "body": "The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.", + "basis": "explicit", + "source": "stakeholder [G2]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 60, + "specId": 1, + "plane": "intent", + "kind": "goal", + "kindOrdinal": 3, + "title": "A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…", + "body": "A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without needing to open a detail panel.", + "basis": "explicit", + "source": "stakeholder [G3]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 66, + "specId": 1, + "plane": "intent", + "kind": "goal", + "kindOrdinal": 4, + "title": "The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…", + "body": "The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivation, reconciliation, and resolution.", + "basis": "explicit", + "source": "stakeholder [G1]", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 71, + "specId": 1, + "plane": "intent", + "kind": "requirement", + "kindOrdinal": 1, + "title": "On initial load, the macro view should fit the fully expanded derivation graph into view.", + "body": "Because each page load starts fully expanded and the view's launch purpose is to explain how the spec got here, the first rendered viewport should frame the whole derivation story with enough margin that the trunk, phase groups, and nested runs are immediately orientable before the user pans or zooms.", + "basis": "implicit", + "source": "derived from selected spec", + "createdAtLsn": 3, + "updatedAtLsn": 3 + }, + { + "id": 72, + "specId": 1, + "plane": "intent", + "kind": "requirement", + "kindOrdinal": 2, + "title": "When artifact data is missing or unrendereable, the macro view should show a read-only diagnostic state instead of a blank canvas.", + "body": "The diagnostic should name the missing or invalid derivation records at a user-facing level while preserving the macro view's read-only, snapshot-only behavior. This keeps first launch useful even when the artifact cannot produce the intended graph.", + "basis": "implicit", + "source": "derived from selected spec", + "createdAtLsn": 3, + "updatedAtLsn": 3 + }, + { + "id": 3, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 1, + "title": "The spec-elicitation system's derivation process consists of four phases in str…", + "basis": "explicit", + "source": "external-observed [T2]", + "detail": { + "definition": "The spec-elicitation system's derivation process consists of four phases in strict dependency order: grounding, shaping, pinning, and defining_done." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 7, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 2, + "title": "The HubNode type has hubType of justification | decision | impasse | perspectiv…", + "basis": "explicit", + "source": "technical-observed [T7]", + "detail": { + "definition": "The HubNode type has hubType of justification | decision | impasse | perspective, and carries impasseStatus (open | resolved | superseded) and perspectiveStatus (open | selected | rejected)." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 9, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 3, + "title": "The onion-peel structure refers to the iterative cycle of impasse discovery, re…", + "basis": "explicit", + "source": "external-observed [T13]", + "detail": { + "definition": "The onion-peel structure refers to the iterative cycle of impasse discovery, rederivation, fan-out/fan-in synthesis, reconciliation, and resolution that builds the derivation history." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 12, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 4, + "title": "The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…", + "basis": "explicit", + "source": "technical-observed [T6]", + "detail": { + "definition": "The ReconciliationRecord type captures reconciliation outcomes (accepted | retry | recurse | bail) along with candidateNodeIds, baselineNodeIds, activatedNodeIds, archivedNodeIds, triggerImpasseIds, resolvedImpasseIds, unresolvedImpasseIds, and a materialProgress flag." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 22, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 5, + "title": "The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…", + "basis": "explicit", + "source": "technical-observed [T3]", + "detail": { + "definition": "The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase, triggerImpasseIds, mode (initial | rederive | grounding_enrichment), attemptNumber, nudgingActive, createdAt, and summary." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 23, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 6, + "title": "The ArtifactFile type bundles all spec data into a single file: manifest, sourc…", + "basis": "explicit", + "source": "technical-observed [T8]", + "detail": { + "definition": "The ArtifactFile type bundles all spec data into a single file: manifest, sources, extractedClaims, interventions, and a graph object containing nodes, edges, frames, derivationRuns, fanInRecords, reconciliationRecords, and snapshots." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 24, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 7, + "title": "The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…", + "basis": "explicit", + "source": "technical-observed [T4]", + "detail": { + "definition": "The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeIds, outputCandidateIds, impassesFound, status (running | completed | failed), and createdAt." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 25, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 8, + "title": "A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…", + "basis": "explicit", + "source": "stakeholder [T11]", + "detail": { + "definition": "A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the system gave up on that branch entirely." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 48, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 9, + "title": "The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…", + "basis": "explicit", + "source": "technical-observed [T5]", + "detail": { + "definition": "The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds, groupings (with resolution: merged | best_selected | impasse_surfaced), outputCandidateIds, droppedCandidateIds, and createdAt." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 53, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 10, + "title": "materialProgress=true on a ReconciliationRecord means at least some nodes were…", + "basis": "explicit", + "source": "stakeholder [T12]", + "detail": { + "definition": "materialProgress=true on a ReconciliationRecord means at least some nodes were activated or archived during that reconciliation — real forward progress occurred." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 58, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 11, + "title": "The Phase type has four ordered values: grounding, shaping, pinning, and defini…", + "basis": "explicit", + "source": "technical-observed [T1]", + "detail": { + "definition": "The Phase type has four ordered values: grounding, shaping, pinning, and defining_done." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 62, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 12, + "title": "The macro view is the component within the Spec Explorer UI that narrates the f…", + "basis": "explicit", + "source": "stakeholder [T14]", + "detail": { + "definition": "The macro view is the component within the Spec Explorer UI that narrates the full derivation history of a spec via a pannable, zoomable React Flow graph of ~20–40 semantically typed nodes." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 63, + "specId": 1, + "plane": "intent", + "kind": "term", + "kindOrdinal": 13, + "title": "A phantom node represents the case where no perspective is selected — it appear…", + "basis": "explicit", + "source": "stakeholder [T10]", + "detail": { + "definition": "A phantom node represents the case where no perspective is selected — it appears when an alternative perspective is used." + }, + "createdAtLsn": 2, + "updatedAtLsn": 2 + } + ], + "edges": [ + { + "id": 1, + "specId": 1, + "category": "dependency", + "sourceId": 16, + "targetId": 28, + "basis": "explicit", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 2, + "specId": 1, + "category": "dependency", + "sourceId": 16, + "targetId": 50, + "basis": "explicit", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 3, + "specId": 1, + "category": "dependency", + "sourceId": 45, + "targetId": 32, + "basis": "explicit", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 4, + "specId": 1, + "category": "dependency", + "sourceId": 61, + "targetId": 67, + "basis": "explicit", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 5, + "specId": 1, + "category": "dependency", + "sourceId": 16, + "targetId": 32, + "basis": "explicit", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 6, + "specId": 1, + "category": "dependency", + "sourceId": 45, + "targetId": 16, + "basis": "explicit", + "createdAtLsn": 2, + "updatedAtLsn": 2 + }, + { + "id": 7, + "specId": 1, + "category": "dependency", + "sourceId": 71, + "targetId": 21, + "basis": "implicit", + "createdAtLsn": 3, + "updatedAtLsn": 3, + "rationale": "The viewport requirement is specifically about the existing fully-expanded initial-load behavior." + }, + { + "id": 8, + "specId": 1, + "category": "dependency", + "sourceId": 71, + "targetId": 49, + "basis": "implicit", + "createdAtLsn": 3, + "updatedAtLsn": 3, + "rationale": "The initial framing sits inside the existing pannable, zoomable graph interaction model." + }, + { + "id": 9, + "specId": 1, + "category": "support", + "sourceId": 71, + "targetId": 66, + "basis": "implicit", + "createdAtLsn": 3, + "updatedAtLsn": 3, + "stance": "for", + "rationale": "An immediately framed graph helps the user answer how the spec got here before any navigation." + }, + { + "id": 10, + "specId": 1, + "category": "dependency", + "sourceId": 72, + "targetId": 23, + "basis": "implicit", + "createdAtLsn": 3, + "updatedAtLsn": 3, + "rationale": "The error state is tied to whether the bundled artifact data can produce the macro graph." + }, + { + "id": 11, + "specId": 1, + "category": "dependency", + "sourceId": 72, + "targetId": 11, + "basis": "implicit", + "createdAtLsn": 3, + "updatedAtLsn": 3, + "rationale": "The diagnostic must not introduce mutation paths into the read-only macro view." + }, + { + "id": 12, + "specId": 1, + "category": "support", + "sourceId": 72, + "targetId": 60, + "basis": "implicit", + "createdAtLsn": 3, + "updatedAtLsn": 3, + "stance": "for", + "rationale": "A named diagnostic is more useful than a blank canvas when node visuals cannot be produced." + } + ], + "nodeCount": 72, + "edgeCount": 12, + "lsn": 3 +} diff --git a/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/report.json b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/report.json new file mode 100644 index 000000000..edc32f970 --- /dev/null +++ b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/report.json @@ -0,0 +1,77 @@ +{ + "schemaVersion": 1, + "probeId": "fixture-curation", + "runId": "fixture-curation-2026-06-05T104440Z", + "generatedAt": "2026-06-05T08:44:38.258Z", + "seedSet": "bilal-port-variants", + "seedSlug": "macro-view-grounded-intent", + "selectedBaseProfile": "grounded-intent", + "cwd": "/var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-fixture-curation-vg9Eo2", + "specId": 1, + "sessionId": "019e96f4-ae08-762d-9783-ddd94163e7b3", + "prompt": "Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named \"macro-view-grounded-intent\".\n\nUse read_graph once in overview mode. Then use commit_graph exactly once to add a small intent-plane expansion that improves launch/usefulness of this existing spec without duplicating base nodes. Create one to three new intent-plane nodes, connect them legally to existing graph truth when possible, use basis implicit through the propose-graph tool path, and stop after a successful commit_graph result.", + "runtimeState": { + "operationalMode": "elicit", + "agentStrategy": "propose-graph", + "agentLens": "intent", + "agentGoal": "commit-converge" + }, + "model": "gpt-5.5", + "success": true, + "commitGraphAttemptCount": 1, + "commitGraphAttempts": [ + { + "index": 1, + "status": "success", + "lsn": 3, + "nodeRefs": { + "n1": 71, + "n2": 72 + }, + "edgeIds": [ + 7, + 8, + 9, + 10, + 11, + 12 + ], + "content": "Graph committed successfully (LSN 3).\nNodes created: n1 → R1, n2 → R2\nEdges created: #7, #8, #9, #10, #11, #12" + } + ], + "createdNodes": [ + { + "id": 71, + "code": "R1", + "plane": "intent", + "kind": "requirement", + "title": "On initial load, the macro view should fit the fully expanded derivation graph into view.", + "basis": "implicit" + }, + { + "id": 72, + "code": "R2", + "plane": "intent", + "kind": "requirement", + "title": "When artifact data is missing or unrendereable, the macro view should show a read-only diagnostic state instead of a blank canvas.", + "basis": "implicit" + } + ], + "finalGraph": { + "nodeCount": 72, + "edgeCount": 12, + "lsn": 3, + "explicitNodeCount": 70, + "implicitNodeCount": 2, + "explicitEdgeCount": 6, + "implicitEdgeCount": 6 + }, + "friction": [], + "artifacts": { + "runDir": "/Users/lunelson/Code/hashintel/brunch-next/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z", + "sessionJsonl": "/Users/lunelson/Code/hashintel/brunch-next/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/session.jsonl", + "transcriptMarkdown": "/Users/lunelson/Code/hashintel/brunch-next/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/transcript.md", + "reportJson": "/Users/lunelson/Code/hashintel/brunch-next/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/report.json", + "graphSnapshotJson": "/Users/lunelson/Code/hashintel/brunch-next/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/graph-snapshot.json" + } +} diff --git a/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/session.jsonl b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/session.jsonl new file mode 100644 index 000000000..aa77b0e16 --- /dev/null +++ b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/session.jsonl @@ -0,0 +1,12 @@ +{"type":"session","version":3,"id":"019e96f4-ae08-762d-9783-ddd94163e7b3","timestamp":"2026-06-05T08:44:38.280Z","cwd":"/var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-fixture-curation-vg9Eo2"} +{"type":"custom","customType":"brunch.session_binding","data":{"schemaVersion":1,"specId":1},"id":"40c16a1b","parentId":null,"timestamp":"2026-06-05T08:44:38.280Z"} +{"type":"session_info","id":"788682ca","parentId":"40c16a1b","timestamp":"2026-06-05T08:44:38.280Z","name":"Macro View — grounded intent base — session 1"} +{"type":"custom","customType":"brunch.agent_runtime_state","data":{"schemaVersion":1,"reason":"switch","state":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"propose-graph","agentLens":"intent","agentGoal":"commit-converge"},"previous":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"auto","agentLens":"auto","agentGoal":"grounding-advance"},"source":"extension"},"id":"2ab04e99","parentId":"788682ca","timestamp":"2026-06-05T08:44:38.281Z"} +{"type":"model_change","id":"0318a8b1","parentId":"2ab04e99","timestamp":"2026-06-05T08:44:38.293Z","provider":"openai-codex","modelId":"gpt-5.5"} +{"type":"thinking_level_change","id":"a030d5cd","parentId":"0318a8b1","timestamp":"2026-06-05T08:44:38.293Z","thinkingLevel":"medium"} +{"type":"message","id":"36bf0036","parentId":"a030d5cd","timestamp":"2026-06-05T08:44:38.301Z","message":{"role":"user","content":[{"type":"text","text":"Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named \"macro-view-grounded-intent\".\n\nUse read_graph once in overview mode. Then use commit_graph exactly once to add a small intent-plane expansion that improves launch/usefulness of this existing spec without duplicating base nodes. Create one to three new intent-plane nodes, connect them legally to existing graph truth when possible, use basis implicit through the propose-graph tool path, and stop after a successful commit_graph result."}],"timestamp":1780649078294}} +{"type":"message","id":"cd033413","parentId":"36bf0036","timestamp":"2026-06-05T08:44:43.220Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"","thinkingSignature":"{\"id\":\"rs_07abc342075b133e016a228c7936b481919a1a099825ccfbbb\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqIox737jyLo4X9eaGR6VyJ4t7xetyq6VbYZdOzIF-pKb-pkDCdyx9F2Msm-ML1tU-jpyb13kfDg6lBaRl9RUvgzzOBIWisKvf04YGHRHEsLJi3f9qQ-sGYEfFeDXUVrSv4sFN4_5r_JRa9-mkbOX_xb-nT-Fkc32wlKFK8A6TSNf8PLpuV7ILDdzaSzYNpi0MCHU6DNZ_e2R6RBUmoVhQhlIFpyZIRPeNjQIJSuwr51OdEn2GanKcGI7PFFDCl_MapX9AOuPJ00AhyfYLqPxemrCpnqzIKwx9oiDyzmsKWlAZl3c8CbiwJbZqNeFuaD4XjB65-xukbg2McRm2kROS3d-5skogRoJ9cj5WhxaOcX1snTJZSbMUMElfGFjY8QSSCpFXeF89N531T7DDLIB9h9QmAOO-yPdPoaNxXxz2FcJL8tv1jKYsGNH8kLjVE2QkuYHCQH3iS5Hm6lFFgJfXhp6i0OETsUcdMFtoZZebnloHQnkfE0h1iOVmiN04LEGZ1qF46lJEOkRpYzpFbqa25DDfGIlof7QJGbLtm1tax7DKhFhRgTyXRN_efHAThnaey9ngGRP3TJ-i6hccDUfxqfRq2SHcWdGNd6O0aMipvNcgqycQdXM6j6hyOo_UO4tlzcCIxva-i9CSlKMT1IpaPQ23NidnCvR5c62x8VAU6cumIz7kmSNBXHepccALILcfHvEA62QlYL53-f9dd2FqNU9JiX4P1X9eOYbySOHeF1bbRLAeI9Bp34YLwr4Kdklbl0NsKaUscFOpYBcwOawJwBvUqZmoiFqQxI94e3qleYjzvpSgm6Ho2JZFpodYRoxSS4hZrfYRcnHM4yGAYOWi5kddKJlOzWkSx_UcdllY0GK_-osk4oX5pCHQfHrpsZADWz6ftc_xSYU9S8PQwVKmSkoQ8UC0fprntxOnaQw19Ijv3Ww4OQSTd64n3vG2QcEhr_8cL0D7etMPQ5kEPLMkRa1SZTVeBHSYsXkmHEkfvgzjqLPnL8l2780cUJpyngb5gS3m\",\"summary\":[]}"},{"type":"toolCall","id":"call_1O5fXny2BiVTwVfzTUJTeW8n|fc_07abc342075b133e016a228c7b5c988191af6cda0f5efa5e77","name":"read_graph","arguments":{"mode":"overview"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.5","usage":{"input":4758,"output":41,"cacheRead":0,"cacheWrite":0,"totalTokens":4799,"cost":{"input":0.023790000000000002,"output":0.00123,"cacheRead":0,"cacheWrite":0,"total":0.02502}},"stopReason":"toolUse","timestamp":1780649078312,"responseId":"resp_07abc342075b133e016a228c78042c81918f44d554afbca658"}} +{"type":"message","id":"9048aadd","parentId":"cd033413","timestamp":"2026-06-05T08:44:43.224Z","message":{"role":"toolResult","toolCallId":"call_1O5fXny2BiVTwVfzTUJTeW8n|fc_07abc342075b133e016a228c7b5c988191af6cda0f5efa5e77","toolName":"read_graph","content":[{"type":"text","text":"Graph overview (LSN 2): 70 node(s), 6 edge(s).\n\n- [CON1] intent/constraint: \"The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…\"\n- [CON2] intent/constraint: \"This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…\"\n- [CON3] intent/constraint: \"The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.\"\n- [CON4] intent/constraint: \"Users must manually refresh to see new derivation steps in the macro view.\"\n- [CON5] intent/constraint: \"The macro view is built inside a Vite + React + Tailwind SPA.\"\n- [CON6] intent/constraint: \"Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.\"\n- [CON7] intent/constraint: \"Phase color values must be expressed as oklch values within the phosphor palette.\"\n- [CON8] intent/constraint: \"The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…\"\n- [CON9] intent/constraint: \"The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…\"\n- [CON10] intent/constraint: \"Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.\"\n- [CON11] intent/constraint: \"The macro view must use React Flow (@xyflow/react) version ^12.\"\n- [CON12] intent/constraint: \"The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.\"\n- [CTX1] intent/context: \"In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…\"\n- [CTX2] intent/context: \"The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…\"\n- [CTX3] intent/context: \"Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…\"\n- [CTX4] intent/context: \"Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.\"\n- [CTX5] intent/context: \"Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.\"\n- [CTX6] intent/context: \"Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.\"\n- [CTX7] intent/context: \"The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.\"\n- [CTX8] intent/context: \"src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…\"\n- [CTX9] intent/context: \"Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.\"\n- [CTX10] intent/context: \"Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.\"\n- [CTX11] intent/context: \"Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…\"\n- [CTX12] intent/context: \"Stakeholder preference: a failed run is shown with a red color and a dimmed interior.\"\n- [CTX13] intent/context: \"Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…\"\n- [CTX14] intent/context: \"A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…\"\n- [CTX15] intent/context: \"The macro view is one specific view within the broader Spec Explorer UI.\"\n- [CTX16] intent/context: \"Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…\"\n- [CTX17] intent/context: \"Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…\"\n- [CTX18] intent/context: \"Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.\"\n- [CTX19] intent/context: \"Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…\"\n- [CTX20] intent/context: \"The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…\"\n- [CTX21] intent/context: \"Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…\"\n- [CTX22] intent/context: \"Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.\"\n- [CTX23] intent/context: \"Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…\"\n- [CTX24] intent/context: \"Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.\"\n- [CTX25] intent/context: \"Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…\"\n- [CTX26] intent/context: \"Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.\"\n- [CTX27] intent/context: \"Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…\"\n- [CTX28] intent/context: \"The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…\"\n- [CTX29] intent/context: \"The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…\"\n- [CTX30] intent/context: \"Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.\"\n- [CTX31] intent/context: \"Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.\"\n- [CTX32] intent/context: \"Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.\"\n- [CTX33] intent/context: \"Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.\"\n- [CTX34] intent/context: \"Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…\"\n- [CTX35] intent/context: \"Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.\"\n- [CTX36] intent/context: \"Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…\"\n- [CTX37] intent/context: \"A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…\"\n- [CTX38] intent/context: \"Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…\"\n- [CTX39] intent/context: \"A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.\"\n- [CTX40] intent/context: \"Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…\"\n- [CTX41] intent/context: \"Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…\"\n- [G1] intent/goal: \"Each node's form and function must visually communicate what happened at that specific point in the derivation tree.\"\n- [G2] intent/goal: \"The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.\"\n- [G3] intent/goal: \"A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…\"\n- [G4] intent/goal: \"The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…\"\n- [T1] intent/term: \"The spec-elicitation system's derivation process consists of four phases in str…\" [has detail]\n- [T2] intent/term: \"The HubNode type has hubType of justification | decision | impasse | perspectiv…\" [has detail]\n- [T3] intent/term: \"The onion-peel structure refers to the iterative cycle of impasse discovery, re…\" [has detail]\n- [T4] intent/term: \"The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…\" [has detail]\n- [T5] intent/term: \"The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…\" [has detail]\n- [T6] intent/term: \"The ArtifactFile type bundles all spec data into a single file: manifest, sourc…\" [has detail]\n- [T7] intent/term: \"The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…\" [has detail]\n- [T8] intent/term: \"A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…\" [has detail]\n- [T9] intent/term: \"The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…\" [has detail]\n- [T10] intent/term: \"materialProgress=true on a ReconciliationRecord means at least some nodes were…\" [has detail]\n- [T11] intent/term: \"The Phase type has four ordered values: grounding, shaping, pinning, and defini…\" [has detail]\n- [T12] intent/term: \"The macro view is the component within the Spec Explorer UI that narrates the f…\" [has detail]\n- [T13] intent/term: \"A phantom node represents the case where no perspective is selected — it appear…\" [has detail]\n\n- Edge #1: CTX8 —[dependency]→ CON7\n- Edge #2: CTX8 —[dependency]→ CON9\n- Edge #3: CTX28 —[dependency]→ CTX17\n- Edge #4: CTX37 —[dependency]→ CTX39\n- Edge #5: CTX8 —[dependency]→ CTX17\n- Edge #6: CTX28 —[dependency]→ CTX8"}],"details":{"nodes":[{"id":6,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":1,"title":"The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…","body":"The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded corners, or blue primary buttons.","basis":"explicit","source":"external-observed [C6]","createdAtLsn":2,"updatedAtLsn":2},{"id":8,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":2,"title":"This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…","body":"This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitly out of scope.","basis":"explicit","source":"stakeholder [C5]","createdAtLsn":2,"updatedAtLsn":2},{"id":11,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":3,"title":"The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.","body":"The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.","basis":"explicit","source":"stakeholder [C10]","createdAtLsn":2,"updatedAtLsn":2},{"id":13,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":4,"title":"Users must manually refresh to see new derivation steps in the macro view.","body":"Users must manually refresh to see new derivation steps in the macro view.","basis":"explicit","source":"stakeholder [C12]","createdAtLsn":2,"updatedAtLsn":2},{"id":19,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":5,"title":"The macro view is built inside a Vite + React + Tailwind SPA.","body":"The macro view is built inside a Vite + React + Tailwind SPA.","basis":"explicit","source":"stakeholder [C2]","createdAtLsn":2,"updatedAtLsn":2},{"id":21,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":6,"title":"Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.","body":"Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.","basis":"explicit","source":"stakeholder [C9]","createdAtLsn":2,"updatedAtLsn":2},{"id":28,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":7,"title":"Phase color values must be expressed as oklch values within the phosphor palette.","body":"Phase color values must be expressed as oklch values within the phosphor palette.","basis":"explicit","source":"stakeholder [C13]","createdAtLsn":2,"updatedAtLsn":2},{"id":38,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":8,"title":"The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…","body":"The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes and vertical flow.","basis":"explicit","source":"stakeholder [C3]","createdAtLsn":2,"updatedAtLsn":2},{"id":50,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":9,"title":"The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…","body":"The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no generic UI patterns.","basis":"explicit","source":"stakeholder [C4]","createdAtLsn":2,"updatedAtLsn":2},{"id":56,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":10,"title":"Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.","body":"Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.","basis":"explicit","source":"stakeholder [C8]","createdAtLsn":2,"updatedAtLsn":2},{"id":64,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":11,"title":"The macro view must use React Flow (@xyflow/react) version ^12.","body":"The macro view must use React Flow (@xyflow/react) version ^12.","basis":"explicit","source":"stakeholder [C1]","createdAtLsn":2,"updatedAtLsn":2},{"id":70,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":12,"title":"The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.","body":"The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.","basis":"explicit","source":"stakeholder [C11]","createdAtLsn":2,"updatedAtLsn":2},{"id":1,"specId":1,"plane":"intent","kind":"context","kindOrdinal":1,"title":"In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…","body":"In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a separate LLM agent that synthesizes the minimal viable set before reconciliation.","basis":"explicit","source":"external-observed [X7]","createdAtLsn":2,"updatedAtLsn":2},{"id":2,"specId":1,"plane":"intent","kind":"context","kindOrdinal":2,"title":"The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…","body":"The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosphor glow behavior, scanline texture, and JetBrains Mono font.","basis":"explicit","source":"external-observed [X6]","createdAtLsn":2,"updatedAtLsn":2},{"id":4,"specId":1,"plane":"intent","kind":"context","kindOrdinal":3,"title":"Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…","body":"Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor glow, scanline textures).","basis":"explicit","source":"stakeholder [X11]","createdAtLsn":2,"updatedAtLsn":2},{"id":5,"specId":1,"plane":"intent","kind":"context","kindOrdinal":4,"title":"Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.","body":"Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.","basis":"explicit","source":"stakeholder [X29]","createdAtLsn":2,"updatedAtLsn":2},{"id":10,"specId":1,"plane":"intent","kind":"context","kindOrdinal":5,"title":"Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.","body":"Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.","basis":"explicit","source":"stakeholder [X36]","createdAtLsn":2,"updatedAtLsn":2},{"id":14,"specId":1,"plane":"intent","kind":"context","kindOrdinal":6,"title":"Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.","body":"Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.","basis":"explicit","source":"stakeholder [X31]","createdAtLsn":2,"updatedAtLsn":2},{"id":15,"specId":1,"plane":"intent","kind":"context","kindOrdinal":7,"title":"The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.","body":"The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.","basis":"explicit","source":"stakeholder [X2]","createdAtLsn":2,"updatedAtLsn":2},{"id":16,"specId":1,"plane":"intent","kind":"context","kindOrdinal":8,"title":"src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…","body":"src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping (amber), --color-phase-pinning (cyan), --color-phase-defining-done (purple), plus --color-phosphor-red, surface-0..3 warm darks, and --font-mono JetBrains Mono.","basis":"explicit","source":"technical-observed [X39]","createdAtLsn":2,"updatedAtLsn":2},{"id":17,"specId":1,"plane":"intent","kind":"context","kindOrdinal":9,"title":"Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.","body":"Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.","basis":"explicit","source":"stakeholder [X33]","createdAtLsn":2,"updatedAtLsn":2},{"id":18,"specId":1,"plane":"intent","kind":"context","kindOrdinal":10,"title":"Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.","body":"Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.","basis":"explicit","source":"stakeholder [X18]","createdAtLsn":2,"updatedAtLsn":2},{"id":20,"specId":1,"plane":"intent","kind":"context","kindOrdinal":11,"title":"Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…","body":"Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the outcome indicator.","basis":"explicit","source":"stakeholder [X35]","createdAtLsn":2,"updatedAtLsn":2},{"id":26,"specId":1,"plane":"intent","kind":"context","kindOrdinal":12,"title":"Stakeholder preference: a failed run is shown with a red color and a dimmed interior.","body":"Stakeholder preference: a failed run is shown with a red color and a dimmed interior.","basis":"explicit","source":"stakeholder [X27]","createdAtLsn":2,"updatedAtLsn":2},{"id":27,"specId":1,"plane":"intent","kind":"context","kindOrdinal":13,"title":"Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…","body":"Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information density is high but not overwhelming.","basis":"explicit","source":"stakeholder [X37]","createdAtLsn":2,"updatedAtLsn":2},{"id":29,"specId":1,"plane":"intent","kind":"context","kindOrdinal":14,"title":"A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…","body":"A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story IR → Layout → Render with React Flow custom node types; the file is otherwise a stub.","basis":"explicit","source":"technical-observed [X38]","createdAtLsn":2,"updatedAtLsn":2},{"id":30,"specId":1,"plane":"intent","kind":"context","kindOrdinal":15,"title":"The macro view is one specific view within the broader Spec Explorer UI.","body":"The macro view is one specific view within the broader Spec Explorer UI.","basis":"explicit","source":"stakeholder [X1]","createdAtLsn":2,"updatedAtLsn":2},{"id":31,"specId":1,"plane":"intent","kind":"context","kindOrdinal":16,"title":"Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…","body":"Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue border, bail=red border + dimmed interior.","basis":"explicit","source":"stakeholder [X28]","createdAtLsn":2,"updatedAtLsn":2},{"id":32,"specId":1,"plane":"intent","kind":"context","kindOrdinal":17,"title":"Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…","body":"Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/commitment), defining_done=violet (completion/closure).","basis":"explicit","source":"stakeholder [X34]","createdAtLsn":2,"updatedAtLsn":2},{"id":33,"specId":1,"plane":"intent","kind":"context","kindOrdinal":18,"title":"Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.","body":"Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.","basis":"explicit","source":"stakeholder [X26]","createdAtLsn":2,"updatedAtLsn":2},{"id":34,"specId":1,"plane":"intent","kind":"context","kindOrdinal":19,"title":"Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…","body":"Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constructs.","basis":"explicit","source":"stakeholder [X15]","createdAtLsn":2,"updatedAtLsn":2},{"id":35,"specId":1,"plane":"intent","kind":"context","kindOrdinal":20,"title":"The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…","body":"The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than graph content.","basis":"explicit","source":"stakeholder [X3]","createdAtLsn":2,"updatedAtLsn":2},{"id":36,"specId":1,"plane":"intent","kind":"context","kindOrdinal":21,"title":"Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…","body":"Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relationship using lineage edge types.","basis":"explicit","source":"external-observed [X8]","createdAtLsn":2,"updatedAtLsn":2},{"id":37,"specId":1,"plane":"intent","kind":"context","kindOrdinal":22,"title":"Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.","body":"Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.","basis":"explicit","source":"stakeholder [X30]","createdAtLsn":2,"updatedAtLsn":2},{"id":39,"specId":1,"plane":"intent","kind":"context","kindOrdinal":23,"title":"Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…","body":"Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation branch.","basis":"explicit","source":"stakeholder [X16]","createdAtLsn":2,"updatedAtLsn":2},{"id":40,"specId":1,"plane":"intent","kind":"context","kindOrdinal":24,"title":"Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.","body":"Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.","basis":"explicit","source":"stakeholder [X12]","createdAtLsn":2,"updatedAtLsn":2},{"id":41,"specId":1,"plane":"intent","kind":"context","kindOrdinal":25,"title":"Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…","body":"Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected', red for 'impasse_surfaced' — distinguished by a colored left border or chip.","basis":"explicit","source":"stakeholder [X32]","createdAtLsn":2,"updatedAtLsn":2},{"id":42,"specId":1,"plane":"intent","kind":"context","kindOrdinal":26,"title":"Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.","body":"Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.","basis":"explicit","source":"stakeholder [X9]","createdAtLsn":2,"updatedAtLsn":2},{"id":44,"specId":1,"plane":"intent","kind":"context","kindOrdinal":27,"title":"Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…","body":"Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-peel derivation depth.","basis":"explicit","source":"stakeholder [X14]","createdAtLsn":2,"updatedAtLsn":2},{"id":45,"specId":1,"plane":"intent","kind":"context","kindOrdinal":28,"title":"The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…","body":"The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green and pinning=amber. Reconciling this is a minor concern but theme.css is the source of truth for the existing design system.","basis":"explicit","source":"technical-observed [X41]","createdAtLsn":2,"updatedAtLsn":2},{"id":46,"specId":1,"plane":"intent","kind":"context","kindOrdinal":29,"title":"The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…","body":"The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md document.","basis":"explicit","source":"external-observed [X5]","createdAtLsn":2,"updatedAtLsn":2},{"id":47,"specId":1,"plane":"intent","kind":"context","kindOrdinal":30,"title":"Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.","body":"Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.","basis":"explicit","source":"stakeholder [X24]","createdAtLsn":2,"updatedAtLsn":2},{"id":49,"specId":1,"plane":"intent","kind":"context","kindOrdinal":31,"title":"Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.","body":"Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.","basis":"explicit","source":"stakeholder [X10]","createdAtLsn":2,"updatedAtLsn":2},{"id":51,"specId":1,"plane":"intent","kind":"context","kindOrdinal":32,"title":"Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.","body":"Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.","basis":"explicit","source":"stakeholder [X22]","createdAtLsn":2,"updatedAtLsn":2},{"id":52,"specId":1,"plane":"intent","kind":"context","kindOrdinal":33,"title":"Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.","body":"Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.","basis":"explicit","source":"stakeholder [X17]","createdAtLsn":2,"updatedAtLsn":2},{"id":54,"specId":1,"plane":"intent","kind":"context","kindOrdinal":34,"title":"Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…","body":"Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could be materialized later.","basis":"explicit","source":"stakeholder [X23]","createdAtLsn":2,"updatedAtLsn":2},{"id":55,"specId":1,"plane":"intent","kind":"context","kindOrdinal":35,"title":"Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.","body":"Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.","basis":"explicit","source":"stakeholder [X19]","createdAtLsn":2,"updatedAtLsn":2},{"id":59,"specId":1,"plane":"intent","kind":"context","kindOrdinal":36,"title":"Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…","body":"Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING' or '⚡ NUDGE' in phosphor amber) rather than as an external indicator.","basis":"explicit","source":"stakeholder [X25]","createdAtLsn":2,"updatedAtLsn":2},{"id":61,"specId":1,"plane":"intent","kind":"context","kindOrdinal":37,"title":"A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…","body":"A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasible.","basis":"explicit","source":"technical-observed [X40]","createdAtLsn":2,"updatedAtLsn":2},{"id":65,"specId":1,"plane":"intent","kind":"context","kindOrdinal":38,"title":"Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…","body":"Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with mode differences expressed via color, label, or border style only.","basis":"explicit","source":"stakeholder [X20]","createdAtLsn":2,"updatedAtLsn":2},{"id":67,"specId":1,"plane":"intent","kind":"context","kindOrdinal":39,"title":"A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.","body":"A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.","basis":"explicit","source":"stakeholder [X4]","createdAtLsn":2,"updatedAtLsn":2},{"id":68,"specId":1,"plane":"intent","kind":"context","kindOrdinal":40,"title":"Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…","body":"Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, outcome).","basis":"explicit","source":"stakeholder [X21]","createdAtLsn":2,"updatedAtLsn":2},{"id":69,"specId":1,"plane":"intent","kind":"context","kindOrdinal":41,"title":"Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…","body":"Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in, reconciliation, perspective, phantom, and dead-end impasse.","basis":"explicit","source":"stakeholder [X13]","createdAtLsn":2,"updatedAtLsn":2},{"id":43,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":1,"title":"Each node's form and function must visually communicate what happened at that specific point in the derivation tree.","body":"Each node's form and function must visually communicate what happened at that specific point in the derivation tree.","basis":"explicit","source":"stakeholder [G4]","createdAtLsn":2,"updatedAtLsn":2},{"id":57,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":2,"title":"The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.","body":"The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.","basis":"explicit","source":"stakeholder [G2]","createdAtLsn":2,"updatedAtLsn":2},{"id":60,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":3,"title":"A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…","body":"A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without needing to open a detail panel.","basis":"explicit","source":"stakeholder [G3]","createdAtLsn":2,"updatedAtLsn":2},{"id":66,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":4,"title":"The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…","body":"The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivation, reconciliation, and resolution.","basis":"explicit","source":"stakeholder [G1]","createdAtLsn":2,"updatedAtLsn":2},{"id":3,"specId":1,"plane":"intent","kind":"term","kindOrdinal":1,"title":"The spec-elicitation system's derivation process consists of four phases in str…","basis":"explicit","source":"external-observed [T2]","detail":{"definition":"The spec-elicitation system's derivation process consists of four phases in strict dependency order: grounding, shaping, pinning, and defining_done."},"createdAtLsn":2,"updatedAtLsn":2},{"id":7,"specId":1,"plane":"intent","kind":"term","kindOrdinal":2,"title":"The HubNode type has hubType of justification | decision | impasse | perspectiv…","basis":"explicit","source":"technical-observed [T7]","detail":{"definition":"The HubNode type has hubType of justification | decision | impasse | perspective, and carries impasseStatus (open | resolved | superseded) and perspectiveStatus (open | selected | rejected)."},"createdAtLsn":2,"updatedAtLsn":2},{"id":9,"specId":1,"plane":"intent","kind":"term","kindOrdinal":3,"title":"The onion-peel structure refers to the iterative cycle of impasse discovery, re…","basis":"explicit","source":"external-observed [T13]","detail":{"definition":"The onion-peel structure refers to the iterative cycle of impasse discovery, rederivation, fan-out/fan-in synthesis, reconciliation, and resolution that builds the derivation history."},"createdAtLsn":2,"updatedAtLsn":2},{"id":12,"specId":1,"plane":"intent","kind":"term","kindOrdinal":4,"title":"The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…","basis":"explicit","source":"technical-observed [T6]","detail":{"definition":"The ReconciliationRecord type captures reconciliation outcomes (accepted | retry | recurse | bail) along with candidateNodeIds, baselineNodeIds, activatedNodeIds, archivedNodeIds, triggerImpasseIds, resolvedImpasseIds, unresolvedImpasseIds, and a materialProgress flag."},"createdAtLsn":2,"updatedAtLsn":2},{"id":22,"specId":1,"plane":"intent","kind":"term","kindOrdinal":5,"title":"The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…","basis":"explicit","source":"technical-observed [T3]","detail":{"definition":"The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase, triggerImpasseIds, mode (initial | rederive | grounding_enrichment), attemptNumber, nudgingActive, createdAt, and summary."},"createdAtLsn":2,"updatedAtLsn":2},{"id":23,"specId":1,"plane":"intent","kind":"term","kindOrdinal":6,"title":"The ArtifactFile type bundles all spec data into a single file: manifest, sourc…","basis":"explicit","source":"technical-observed [T8]","detail":{"definition":"The ArtifactFile type bundles all spec data into a single file: manifest, sources, extractedClaims, interventions, and a graph object containing nodes, edges, frames, derivationRuns, fanInRecords, reconciliationRecords, and snapshots."},"createdAtLsn":2,"updatedAtLsn":2},{"id":24,"specId":1,"plane":"intent","kind":"term","kindOrdinal":7,"title":"The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…","basis":"explicit","source":"technical-observed [T4]","detail":{"definition":"The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeIds, outputCandidateIds, impassesFound, status (running | completed | failed), and createdAt."},"createdAtLsn":2,"updatedAtLsn":2},{"id":25,"specId":1,"plane":"intent","kind":"term","kindOrdinal":8,"title":"A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…","basis":"explicit","source":"stakeholder [T11]","detail":{"definition":"A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the system gave up on that branch entirely."},"createdAtLsn":2,"updatedAtLsn":2},{"id":48,"specId":1,"plane":"intent","kind":"term","kindOrdinal":9,"title":"The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…","basis":"explicit","source":"technical-observed [T5]","detail":{"definition":"The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds, groupings (with resolution: merged | best_selected | impasse_surfaced), outputCandidateIds, droppedCandidateIds, and createdAt."},"createdAtLsn":2,"updatedAtLsn":2},{"id":53,"specId":1,"plane":"intent","kind":"term","kindOrdinal":10,"title":"materialProgress=true on a ReconciliationRecord means at least some nodes were…","basis":"explicit","source":"stakeholder [T12]","detail":{"definition":"materialProgress=true on a ReconciliationRecord means at least some nodes were activated or archived during that reconciliation — real forward progress occurred."},"createdAtLsn":2,"updatedAtLsn":2},{"id":58,"specId":1,"plane":"intent","kind":"term","kindOrdinal":11,"title":"The Phase type has four ordered values: grounding, shaping, pinning, and defini…","basis":"explicit","source":"technical-observed [T1]","detail":{"definition":"The Phase type has four ordered values: grounding, shaping, pinning, and defining_done."},"createdAtLsn":2,"updatedAtLsn":2},{"id":62,"specId":1,"plane":"intent","kind":"term","kindOrdinal":12,"title":"The macro view is the component within the Spec Explorer UI that narrates the f…","basis":"explicit","source":"stakeholder [T14]","detail":{"definition":"The macro view is the component within the Spec Explorer UI that narrates the full derivation history of a spec via a pannable, zoomable React Flow graph of ~20–40 semantically typed nodes."},"createdAtLsn":2,"updatedAtLsn":2},{"id":63,"specId":1,"plane":"intent","kind":"term","kindOrdinal":13,"title":"A phantom node represents the case where no perspective is selected — it appear…","basis":"explicit","source":"stakeholder [T10]","detail":{"definition":"A phantom node represents the case where no perspective is selected — it appears when an alternative perspective is used."},"createdAtLsn":2,"updatedAtLsn":2}],"edges":[{"id":1,"specId":1,"category":"dependency","sourceId":16,"targetId":28,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":2,"specId":1,"category":"dependency","sourceId":16,"targetId":50,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":3,"specId":1,"category":"dependency","sourceId":45,"targetId":32,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":4,"specId":1,"category":"dependency","sourceId":61,"targetId":67,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":5,"specId":1,"category":"dependency","sourceId":16,"targetId":32,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":6,"specId":1,"category":"dependency","sourceId":45,"targetId":16,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2}],"nodeCount":70,"edgeCount":6,"lsn":2},"isError":false,"timestamp":1780649083224}} +{"type":"message","id":"7226ad3d","parentId":"9048aadd","timestamp":"2026-06-05T08:45:01.565Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Refining launch criteria**\n\nI'm focusing on making sure I commit changes effectively, aiming for a small intent expansion with 1-3 nodes to improve the launch's usefulness. The existing launch doesn't seem effective; maybe it needs a better demo for first-load or error handling. I’m thinking about expanding the intent plane and connecting everything to legal categories and constraints. It may be best to create a requirement that the initial viewport must fit a full derivation, ensuring it’s clear and not duplicative. This way, users see the complete story immediately upon opening.\n\n**Improving canvas launch**\n\nI'm thinking that on the initial load, the canvas should automatically fit the fully expanded derivation graph into view, ensuring there's enough margin for context. This would enhance launch usefulness based on specific constraints like CON6. Also, if the artifact can’t be rendered, it should display a clear diagnostic error instead of just a blank canvas. I'll need to ensure no base nodes are duplicated and explore how the dependencies establish connections to goals and constraints. Finally, it seems we should stop responding after a successful commit.","thinkingSignature":"{\"id\":\"rs_07abc342075b133e016a228c7c6c5c819191399adb02cf55e1\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqIoyK8cC3U6DfQMtM0L9vdVpOOC_0tQ49q8q90NHHxhGCoin6k_1K0BqIHyLg39trQA0GWq4qm6e0WY4uFJpC5nHbIYJNKDCZtw1BvkoNSekxOnba3Bisc-e3eisbh0M1WxcUa9uQzZlodcla89sWA1v54FjbRPsiSDCpX_pnBM5DOtBEybXzauzeBaiNmYCFWawndrg31lSKOFW5JUlTEOdi4P3b7J22K5cLT1Aobef6znxPUnLDx-wBWDtMn2WKv8yukysuoVqdiwHw2K8RwxMtxeoK3UakYS61MMgfbp4qMA19dFGfBTaLYnTJ9iY3ZrFJZ-bZrmEQwQ8SUzXRS6WZxLoWIGqbsHiGX5HAsC0vsxolk1q36SrPqeyS9Do-3duVJ11ErwFzPttyvScLjHSqbQY-JiAU8rhrLGYt5CvHV8pX9EGrsoFwuK5Om_iyDuH-Mcxb0T8AUjbBHRixr1fYw_OjIhn4ChfP5aQ3iHzh6OyPWZWl3Ymm06GpMOaMbCk88wURnBDmTT6nQcM2emmRhOeayWZOy7JlWcMluonmCGxd70Ksts7cKnGPFM3sXWc5Vsnn2KSNxHJb_TIZtjWBCxWBR5xygV5bFTPah8zW9cC7075_KhoQ0u4VhAL3gXVQW-KOPezYJoFP0r9R6q_wlZwvadQsPg7hles8iC1pSRi95x7tREZr49p8mmjZ-Of9ybxjyTQOn9cz63ZtmaWyeQxgUO3v8ZMs6rO57mdrKqS-aF3wKtrh7QMbn_HTYtp5UrSS8H0GWtNUyQQqFabbNBU_S4QSgyf0mHmnkeTpG6wa7IHtBPxb_qotH5rpg2rxOVaHUkgSHH_WRxT7AmPe50k8G2GGyDXN2thXVO4rmirDwFWmQIdhgzVUM_gBtw7zxfZ-MeUnDfCUmLgFs9-n_NmVHXAq1W0HRdaFHNgKzAv6m_FbSTrWmzjOHIqawTinVXNr7NqCoQEZIskl_DCsjSTdCAw7nOFne9Q4uQDrfudTjbCnkmfl-KBBGQx9WJb4tFFp4dRvUEzReCSSCDJe4eVJoGHs-DG33zu4dxkOlPKib_clvRzKY2LR7okxEZD1upkOcp-ANQsLg-nJG07kvgrLelZiW2I9enU2l87PW2142v8myyZB1Zu3-qND3jiHMwNlMJkaCNqJoNJWNEyvZGvdHxZTL0QPbZPqs-incB4sEjM4naMOZaDzRvvNGH78Likw9aRiGv3TbSQRDpIy-SAdfdTI5HMz0LkSCgmHm7-lHcBX2ifpeFvLkavjqHGUa76A3tQDN3dxk5-SpIQQ33jsEM6NX7uqdEJSP0u07EJlI1oMwCN5KIWxMuCMqQsZWGr-IDDIuN3d1nczascMBKs6zANkQQ09m9HBk0ZKAU0-RS6AklvGKWupWjG5WNjEGcfdhhyn3ax_4XT0eMY54d3HbvCMTw78nZlaLAXvBu6VE78hXCz9fqpBexRVws2eLUHh6AUGEzRCNcC7cGo2fZizSq00RYtPYZ_-CHGMdQWByaZBlvApjHJmCrrSqr0vInFBRt-WIEVCRtnLYhEcMyrjlzlDYFelavMbYSaAs8bjCs43oxRhXgqsmlcMDxm5NDQ_jd7Ug-4Re7T-D1Q6GqXOB8lI--WWR2flWLtwxyKmf-yLbjXksLUlE-URoTWyeyFjFyzznB77OElNfukO1Y3JKICPe10WeLRCWVBazlrqY_XtTZyxzCgDf625BU9z4_t6ur-o7Fz2Bcg1RRfS28N6mrRZBN28KY70m2oAK98_FRWs--nrsT_O5VVEGm_UJgzqLhxhmGD3Qi2XGeIJ4NP2DITS9lV6jNk-hVpMHjjHOoqDdWyykABbBLXT0OqdtwTRPC7HTYWmHrNdf7LM5N6yTh7M8SyAgwom_lSduLxwqcVC36enmdGd-RCCTBwE2Rzo7s7RfIVh4L4Fi0UhH4IAjsBAENN3x7Rw41KTyQbU4zNYLoYgCCHBPtrq-ki0SBsQQ50kDMzNODgRYrYrga28Fbapjpool9wZWVq3qutHLvEgMkQb_25WyBBIOOG8jMYuUTshX9JkpK6XIFxBbXEbtcsT9dTbtQqyPcoqXixMaS3cA75iYvlErAXm6tO9hqTC83Vfkn953qSev-sf_4gceJJV9je65NcAPn8AJItQHo7I78sPBORVsavtpw6OkPJYhfNJfG-_TqiazDOILINh_Zkilqtt7EIPuv0ER17-ZZl8CQzddqMOTe1cJc3qA7qaGagP9P5hdoiOQa62Pb2-qAIfuZluf7JzJWvvliWvsDlBuyA9NXslWnEED6AmwKuesGNxGXCTkMoVYxW5y6VUiRdpgElSth98NU8mRV9WJSiy9yYVSRCYF5QXmBlPd_NgnRJW3I3w188i3Y5BZ1JnYFG-krlMBQtKUvWoYEN3DQVYFEhd9IUTylC7ZTorOgSb4EsFLWPHYaZCdk6csnIsFEV2m41ZrjcF4d2E1X8QjSvrV1VDmWju3CVvdouZo1vSKYRvXvckAj1MEbz7I6PmAB-ac3jg3Pifbc3SPhnuvVoW-X5NMKuEdWFiqYENSDEjzkZiEedgG9h4tGBgtOztlE5AQC4YLGaqsGmpdo9ZDcDEGX1oc6hmUhGIahmJdUkK7uuGBcTl-h2_nuNJwFswws74Am72N_UoWAuDDP9lBdKvTvnlD_jERlsxiivckU3qBZwguFGVrgsC4uZo6nBFCG4zoS9KYeiyIawV8CsbJnIdRsa-Q6Lh7W1Hc1_MmYttNgUxvP80enQWP6bXso4_I_KcVQfg_L6PLE_ZjiXSdrcho7w_qLkcgKYA0rMwbd2M3MoMU8xnL4LJdiEbsiL3mJaJZcCDe_UA0GPW22sc33poUEs5lYSr29r1aFidBSKWHDbhLe1L7nkt_5otmc-Xts2FN-vz6b4DvKPf1DpwdD_CXNNBTW_LXzk4DKgF7InRQs4ktR4t-y1KlybzlLlQqmHhNlAC5p6VaCM155CbloAB18x_cDrvVDpO7B55uvBJyIygLtj0XYfFgZ8Ye5ZcWAxLHShCh3t3Ag84IwHL5Jsf_n9nGnJ_TfSl6y5Nv2l7l-7m3FrNZqsl1gS_0YA58BU8IV5zp1ySfuNCFq0jA4PEdj3VSj313uKHWkP_rm3iReu12kdZV_EgRE6lblEX8R-LtAIuGdLCSrWHlHvDS97HN8d32mf712B8o3nwZ4khoMQmcdPbipdctvRK4I2iGvf_oVqYke97I4F0aK5HMJrY5nRLoerqrn8QvTZr9FQztuzcKUc272ICDQKsoKeOqXUyA5Bdkv4V9LRMXh6ysTWEuxI-wGmi6n8SqLSy5lwsUMYHBB6Ks8AsBgvQoRMGDOzLa41JNnvTFrZDTDoS6mspPKbGVQS042K1pe4I0T143qwwbg1AxAx5nP5_4F046ENgg-XZx2DI3xgSIYdirvBQ7S-KTgR4Oub_1OtAxBPwlijxosykGNJeK36A02VGM95nXtsfimsHiQs50joGbiSLe4ErZmJ7tKUVJ0Us-Dpnk9V1kY7PSPS3NCFo2TtTM5KwUePMg5cqA-Tk7OEoDYkhkyzqkcWWrzrrIvDflLpojX_5EsHE0tn5FL5XEwfUbKcukj1T_tspSECLrNvngIcT8GegaRZx-KDwBGsI5lHwJt-8RFL1B0-xO5r3k--1qyQG-qbWIIMIgi--RFQUrFwburqrijMw0riiIIYlsg3ViA74aKYM3wawtu73Jmg-ZY-Os9JjQoePUx2S2eU=\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"**Refining launch criteria**\\n\\nI'm focusing on making sure I commit changes effectively, aiming for a small intent expansion with 1-3 nodes to improve the launch's usefulness. The existing launch doesn't seem effective; maybe it needs a better demo for first-load or error handling. I’m thinking about expanding the intent plane and connecting everything to legal categories and constraints. It may be best to create a requirement that the initial viewport must fit a full derivation, ensuring it’s clear and not duplicative. This way, users see the complete story immediately upon opening.\"},{\"type\":\"summary_text\",\"text\":\"**Improving canvas launch**\\n\\nI'm thinking that on the initial load, the canvas should automatically fit the fully expanded derivation graph into view, ensuring there's enough margin for context. This would enhance launch usefulness based on specific constraints like CON6. Also, if the artifact can’t be rendered, it should display a clear diagnostic error instead of just a blank canvas. I'll need to ensure no base nodes are duplicated and explore how the dependencies establish connections to goals and constraints. Finally, it seems we should stop responding after a successful commit.\"}]}"},{"type":"toolCall","id":"call_3AmhISi57VatkjcR4e1UwfVM|fc_07abc342075b133e016a228c8a712881918f8ae4d8862182b3","name":"commit_graph","arguments":{"nodes":[{"ref":"n1","plane":"intent","kind":"requirement","title":"On initial load, the macro view should fit the fully expanded derivation graph into view.","body":"Because each page load starts fully expanded and the view's launch purpose is to explain how the spec got here, the first rendered viewport should frame the whole derivation story with enough margin that the trunk, phase groups, and nested runs are immediately orientable before the user pans or zooms.","source":"derived from selected spec"},{"ref":"n2","plane":"intent","kind":"requirement","title":"When artifact data is missing or unrendereable, the macro view should show a read-only diagnostic state instead of a blank canvas.","body":"The diagnostic should name the missing or invalid derivation records at a user-facing level while preserving the macro view's read-only, snapshot-only behavior. This keeps first launch useful even when the artifact cannot produce the intended graph.","source":"derived from selected spec"}],"edges":[{"category":"dependency","source":"n1","target":{"existingCode":"CON6"},"rationale":"The viewport requirement is specifically about the existing fully-expanded initial-load behavior."},{"category":"dependency","source":"n1","target":{"existingCode":"CTX31"},"rationale":"The initial framing sits inside the existing pannable, zoomable graph interaction model."},{"category":"support","source":"n1","target":{"existingCode":"G4"},"stance":"for","rationale":"An immediately framed graph helps the user answer how the spec got here before any navigation."},{"category":"dependency","source":"n2","target":{"existingCode":"T6"},"rationale":"The error state is tied to whether the bundled artifact data can produce the macro graph."},{"category":"dependency","source":"n2","target":{"existingCode":"CON3"},"rationale":"The diagnostic must not introduce mutation paths into the read-only macro view."},{"category":"support","source":"n2","target":{"existingCode":"G3"},"stance":"for","rationale":"A named diagnostic is more useful than a blank canvas when node visuals cannot be produced."}]}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.5","usage":{"input":2736,"output":894,"cacheRead":4608,"cacheWrite":0,"totalTokens":8238,"cost":{"input":0.013680000000000001,"output":0.02682,"cacheRead":0.002304,"cacheWrite":0,"total":0.042804}},"stopReason":"toolUse","timestamp":1780649083224,"responseId":"resp_07abc342075b133e016a228c7b8ba08191b97aa384d6a1cff8"}} +{"type":"message","id":"8d679470","parentId":"7226ad3d","timestamp":"2026-06-05T08:45:01.570Z","message":{"role":"toolResult","toolCallId":"call_3AmhISi57VatkjcR4e1UwfVM|fc_07abc342075b133e016a228c8a712881918f8ae4d8862182b3","toolName":"commit_graph","content":[{"type":"text","text":"Graph committed successfully (LSN 3).\nNodes created: n1 → R1, n2 → R2\nEdges created: #7, #8, #9, #10, #11, #12"}],"details":{"status":"success","lsn":3,"createdNodes":{"n1":{"id":71,"code":"R1"},"n2":{"id":72,"code":"R2"}},"edges":[7,8,9,10,11,12]},"isError":false,"timestamp":1780649101570}} +{"type":"message","id":"c382d9d1","parentId":"8d679470","timestamp":"2026-06-05T08:45:02.381Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_07abc342075b133e016a228c8e84108191993ad6f9621b8bd3\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.5","usage":{"input":1125,"output":4,"cacheRead":7168,"cacheWrite":0,"totalTokens":8297,"cost":{"input":0.005625000000000001,"output":0.00012,"cacheRead":0.003584,"cacheWrite":0,"total":0.009329}},"stopReason":"stop","timestamp":1780649101570,"responseId":"resp_07abc342075b133e016a228c8de8a081918496dca22c984e8d"}} diff --git a/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/transcript.md b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/transcript.md new file mode 100644 index 000000000..5e53b1812 --- /dev/null +++ b/.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/transcript.md @@ -0,0 +1,117 @@ +# Transcript — session.jsonl + +## 1. User + +Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named "macro-view-grounded-intent". + +Use read_graph once in overview mode. Then use commit_graph exactly once to add a small intent-plane expansion that improves launch/usefulness of this existing spec without duplicating base nodes. Create one to three new intent-plane nodes, connect them legally to existing graph truth when possible, use basis implicit through the propose-graph tool path, and stop after a successful commit_graph result. + +## 2. Tool result: read_graph + +Graph overview (LSN 2): 70 node(s), 6 edge(s). + +- [CON1] intent/constraint: "The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…" +- [CON2] intent/constraint: "This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…" +- [CON3] intent/constraint: "The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations." +- [CON4] intent/constraint: "Users must manually refresh to see new derivation steps in the macro view." +- [CON5] intent/constraint: "The macro view is built inside a Vite + React + Tailwind SPA." +- [CON6] intent/constraint: "Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes." +- [CON7] intent/constraint: "Phase color values must be expressed as oklch values within the phosphor palette." +- [CON8] intent/constraint: "The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…" +- [CON9] intent/constraint: "The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…" +- [CON10] intent/constraint: "Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted." +- [CON11] intent/constraint: "The macro view must use React Flow (@xyflow/react) version ^12." +- [CON12] intent/constraint: "The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively." +- [CTX1] intent/context: "In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…" +- [CTX2] intent/context: "The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…" +- [CTX3] intent/context: "Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…" +- [CTX4] intent/context: "Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency." +- [CTX5] intent/context: "Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse." +- [CTX6] intent/context: "Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed." +- [CTX7] intent/context: "The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'." +- [CTX8] intent/context: "src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…" +- [CTX9] intent/context: "Stakeholder preference: clicking a node opens a detail panel on the right side of the screen." +- [CTX10] intent/context: "Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view." +- [CTX11] intent/context: "Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…" +- [CTX12] intent/context: "Stakeholder preference: a failed run is shown with a red color and a dimmed interior." +- [CTX13] intent/context: "Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…" +- [CTX14] intent/context: "A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…" +- [CTX15] intent/context: "The macro view is one specific view within the broader Spec Explorer UI." +- [CTX16] intent/context: "Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…" +- [CTX17] intent/context: "Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…" +- [CTX18] intent/context: "Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does." +- [CTX19] intent/context: "Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…" +- [CTX20] intent/context: "The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…" +- [CTX21] intent/context: "Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…" +- [CTX22] intent/context: "Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length." +- [CTX23] intent/context: "Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…" +- [CTX24] intent/context: "Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail." +- [CTX25] intent/context: "Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…" +- [CTX26] intent/context: "Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern." +- [CTX27] intent/context: "Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…" +- [CTX28] intent/context: "The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…" +- [CTX29] intent/context: "The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…" +- [CTX30] intent/context: "Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions." +- [CTX31] intent/context: "Stakeholder preference: render the derivation narrative as a pannable, zoomable graph." +- [CTX32] intent/context: "Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow." +- [CTX33] intent/context: "Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node." +- [CTX34] intent/context: "Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…" +- [CTX35] intent/context: "Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout." +- [CTX36] intent/context: "Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…" +- [CTX37] intent/context: "A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…" +- [CTX38] intent/context: "Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…" +- [CTX39] intent/context: "A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it." +- [CTX40] intent/context: "Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…" +- [CTX41] intent/context: "Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…" +- [G1] intent/goal: "Each node's form and function must visually communicate what happened at that specific point in the derivation tree." +- [G2] intent/goal: "The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics." +- [G3] intent/goal: "A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…" +- [G4] intent/goal: "The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…" +- [T1] intent/term: "The spec-elicitation system's derivation process consists of four phases in str…" [has detail] +- [T2] intent/term: "The HubNode type has hubType of justification | decision | impasse | perspectiv…" [has detail] +- [T3] intent/term: "The onion-peel structure refers to the iterative cycle of impasse discovery, re…" [has detail] +- [T4] intent/term: "The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…" [has detail] +- [T5] intent/term: "The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…" [has detail] +- [T6] intent/term: "The ArtifactFile type bundles all spec data into a single file: manifest, sourc…" [has detail] +- [T7] intent/term: "The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…" [has detail] +- [T8] intent/term: "A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…" [has detail] +- [T9] intent/term: "The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…" [has detail] +- [T10] intent/term: "materialProgress=true on a ReconciliationRecord means at least some nodes were…" [has detail] +- [T11] intent/term: "The Phase type has four ordered values: grounding, shaping, pinning, and defini…" [has detail] +- [T12] intent/term: "The macro view is the component within the Spec Explorer UI that narrates the f…" [has detail] +- [T13] intent/term: "A phantom node represents the case where no perspective is selected — it appear…" [has detail] + +- Edge #1: CTX8 —[dependency]→ CON7 +- Edge #2: CTX8 —[dependency]→ CON9 +- Edge #3: CTX28 —[dependency]→ CTX17 +- Edge #4: CTX37 —[dependency]→ CTX39 +- Edge #5: CTX8 —[dependency]→ CTX17 +- Edge #6: CTX28 —[dependency]→ CTX8 + +## 3. Tool result: commit_graph + +Graph committed successfully (LSN 3). +Nodes created: n1 → R1, n2 → R2 +Edges created: #7, #8, #9, #10, #11, #12 + +## 4. Assistant + +_(empty)_ + + +## Raw session JSONL + +```jsonl +{"type":"session","version":3,"id":"019e96f4-ae08-762d-9783-ddd94163e7b3","timestamp":"2026-06-05T08:44:38.280Z","cwd":"/var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-fixture-curation-vg9Eo2"} +{"type":"custom","customType":"brunch.session_binding","data":{"schemaVersion":1,"specId":1},"id":"40c16a1b","parentId":null,"timestamp":"2026-06-05T08:44:38.280Z"} +{"type":"session_info","id":"788682ca","parentId":"40c16a1b","timestamp":"2026-06-05T08:44:38.280Z","name":"Macro View — grounded intent base — session 1"} +{"type":"custom","customType":"brunch.agent_runtime_state","data":{"schemaVersion":1,"reason":"switch","state":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"propose-graph","agentLens":"intent","agentGoal":"commit-converge"},"previous":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"auto","agentLens":"auto","agentGoal":"grounding-advance"},"source":"extension"},"id":"2ab04e99","parentId":"788682ca","timestamp":"2026-06-05T08:44:38.281Z"} +{"type":"model_change","id":"0318a8b1","parentId":"2ab04e99","timestamp":"2026-06-05T08:44:38.293Z","provider":"openai-codex","modelId":"gpt-5.5"} +{"type":"thinking_level_change","id":"a030d5cd","parentId":"0318a8b1","timestamp":"2026-06-05T08:44:38.293Z","thinkingLevel":"medium"} +{"type":"message","id":"36bf0036","parentId":"a030d5cd","timestamp":"2026-06-05T08:44:38.301Z","message":{"role":"user","content":[{"type":"text","text":"Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named \"macro-view-grounded-intent\".\n\nUse read_graph once in overview mode. Then use commit_graph exactly once to add a small intent-plane expansion that improves launch/usefulness of this existing spec without duplicating base nodes. Create one to three new intent-plane nodes, connect them legally to existing graph truth when possible, use basis implicit through the propose-graph tool path, and stop after a successful commit_graph result."}],"timestamp":1780649078294}} +{"type":"message","id":"cd033413","parentId":"36bf0036","timestamp":"2026-06-05T08:44:43.220Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"","thinkingSignature":"{\"id\":\"rs_07abc342075b133e016a228c7936b481919a1a099825ccfbbb\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqIox737jyLo4X9eaGR6VyJ4t7xetyq6VbYZdOzIF-pKb-pkDCdyx9F2Msm-ML1tU-jpyb13kfDg6lBaRl9RUvgzzOBIWisKvf04YGHRHEsLJi3f9qQ-sGYEfFeDXUVrSv4sFN4_5r_JRa9-mkbOX_xb-nT-Fkc32wlKFK8A6TSNf8PLpuV7ILDdzaSzYNpi0MCHU6DNZ_e2R6RBUmoVhQhlIFpyZIRPeNjQIJSuwr51OdEn2GanKcGI7PFFDCl_MapX9AOuPJ00AhyfYLqPxemrCpnqzIKwx9oiDyzmsKWlAZl3c8CbiwJbZqNeFuaD4XjB65-xukbg2McRm2kROS3d-5skogRoJ9cj5WhxaOcX1snTJZSbMUMElfGFjY8QSSCpFXeF89N531T7DDLIB9h9QmAOO-yPdPoaNxXxz2FcJL8tv1jKYsGNH8kLjVE2QkuYHCQH3iS5Hm6lFFgJfXhp6i0OETsUcdMFtoZZebnloHQnkfE0h1iOVmiN04LEGZ1qF46lJEOkRpYzpFbqa25DDfGIlof7QJGbLtm1tax7DKhFhRgTyXRN_efHAThnaey9ngGRP3TJ-i6hccDUfxqfRq2SHcWdGNd6O0aMipvNcgqycQdXM6j6hyOo_UO4tlzcCIxva-i9CSlKMT1IpaPQ23NidnCvR5c62x8VAU6cumIz7kmSNBXHepccALILcfHvEA62QlYL53-f9dd2FqNU9JiX4P1X9eOYbySOHeF1bbRLAeI9Bp34YLwr4Kdklbl0NsKaUscFOpYBcwOawJwBvUqZmoiFqQxI94e3qleYjzvpSgm6Ho2JZFpodYRoxSS4hZrfYRcnHM4yGAYOWi5kddKJlOzWkSx_UcdllY0GK_-osk4oX5pCHQfHrpsZADWz6ftc_xSYU9S8PQwVKmSkoQ8UC0fprntxOnaQw19Ijv3Ww4OQSTd64n3vG2QcEhr_8cL0D7etMPQ5kEPLMkRa1SZTVeBHSYsXkmHEkfvgzjqLPnL8l2780cUJpyngb5gS3m\",\"summary\":[]}"},{"type":"toolCall","id":"call_1O5fXny2BiVTwVfzTUJTeW8n|fc_07abc342075b133e016a228c7b5c988191af6cda0f5efa5e77","name":"read_graph","arguments":{"mode":"overview"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.5","usage":{"input":4758,"output":41,"cacheRead":0,"cacheWrite":0,"totalTokens":4799,"cost":{"input":0.023790000000000002,"output":0.00123,"cacheRead":0,"cacheWrite":0,"total":0.02502}},"stopReason":"toolUse","timestamp":1780649078312,"responseId":"resp_07abc342075b133e016a228c78042c81918f44d554afbca658"}} +{"type":"message","id":"9048aadd","parentId":"cd033413","timestamp":"2026-06-05T08:44:43.224Z","message":{"role":"toolResult","toolCallId":"call_1O5fXny2BiVTwVfzTUJTeW8n|fc_07abc342075b133e016a228c7b5c988191af6cda0f5efa5e77","toolName":"read_graph","content":[{"type":"text","text":"Graph overview (LSN 2): 70 node(s), 6 edge(s).\n\n- [CON1] intent/constraint: \"The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…\"\n- [CON2] intent/constraint: \"This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…\"\n- [CON3] intent/constraint: \"The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.\"\n- [CON4] intent/constraint: \"Users must manually refresh to see new derivation steps in the macro view.\"\n- [CON5] intent/constraint: \"The macro view is built inside a Vite + React + Tailwind SPA.\"\n- [CON6] intent/constraint: \"Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.\"\n- [CON7] intent/constraint: \"Phase color values must be expressed as oklch values within the phosphor palette.\"\n- [CON8] intent/constraint: \"The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…\"\n- [CON9] intent/constraint: \"The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…\"\n- [CON10] intent/constraint: \"Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.\"\n- [CON11] intent/constraint: \"The macro view must use React Flow (@xyflow/react) version ^12.\"\n- [CON12] intent/constraint: \"The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.\"\n- [CTX1] intent/context: \"In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…\"\n- [CTX2] intent/context: \"The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…\"\n- [CTX3] intent/context: \"Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…\"\n- [CTX4] intent/context: \"Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.\"\n- [CTX5] intent/context: \"Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.\"\n- [CTX6] intent/context: \"Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.\"\n- [CTX7] intent/context: \"The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.\"\n- [CTX8] intent/context: \"src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…\"\n- [CTX9] intent/context: \"Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.\"\n- [CTX10] intent/context: \"Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.\"\n- [CTX11] intent/context: \"Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…\"\n- [CTX12] intent/context: \"Stakeholder preference: a failed run is shown with a red color and a dimmed interior.\"\n- [CTX13] intent/context: \"Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…\"\n- [CTX14] intent/context: \"A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…\"\n- [CTX15] intent/context: \"The macro view is one specific view within the broader Spec Explorer UI.\"\n- [CTX16] intent/context: \"Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…\"\n- [CTX17] intent/context: \"Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…\"\n- [CTX18] intent/context: \"Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.\"\n- [CTX19] intent/context: \"Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…\"\n- [CTX20] intent/context: \"The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…\"\n- [CTX21] intent/context: \"Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…\"\n- [CTX22] intent/context: \"Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.\"\n- [CTX23] intent/context: \"Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…\"\n- [CTX24] intent/context: \"Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.\"\n- [CTX25] intent/context: \"Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…\"\n- [CTX26] intent/context: \"Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.\"\n- [CTX27] intent/context: \"Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…\"\n- [CTX28] intent/context: \"The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…\"\n- [CTX29] intent/context: \"The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…\"\n- [CTX30] intent/context: \"Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.\"\n- [CTX31] intent/context: \"Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.\"\n- [CTX32] intent/context: \"Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.\"\n- [CTX33] intent/context: \"Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.\"\n- [CTX34] intent/context: \"Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…\"\n- [CTX35] intent/context: \"Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.\"\n- [CTX36] intent/context: \"Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…\"\n- [CTX37] intent/context: \"A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…\"\n- [CTX38] intent/context: \"Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…\"\n- [CTX39] intent/context: \"A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.\"\n- [CTX40] intent/context: \"Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…\"\n- [CTX41] intent/context: \"Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…\"\n- [G1] intent/goal: \"Each node's form and function must visually communicate what happened at that specific point in the derivation tree.\"\n- [G2] intent/goal: \"The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.\"\n- [G3] intent/goal: \"A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…\"\n- [G4] intent/goal: \"The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…\"\n- [T1] intent/term: \"The spec-elicitation system's derivation process consists of four phases in str…\" [has detail]\n- [T2] intent/term: \"The HubNode type has hubType of justification | decision | impasse | perspectiv…\" [has detail]\n- [T3] intent/term: \"The onion-peel structure refers to the iterative cycle of impasse discovery, re…\" [has detail]\n- [T4] intent/term: \"The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…\" [has detail]\n- [T5] intent/term: \"The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…\" [has detail]\n- [T6] intent/term: \"The ArtifactFile type bundles all spec data into a single file: manifest, sourc…\" [has detail]\n- [T7] intent/term: \"The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…\" [has detail]\n- [T8] intent/term: \"A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…\" [has detail]\n- [T9] intent/term: \"The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…\" [has detail]\n- [T10] intent/term: \"materialProgress=true on a ReconciliationRecord means at least some nodes were…\" [has detail]\n- [T11] intent/term: \"The Phase type has four ordered values: grounding, shaping, pinning, and defini…\" [has detail]\n- [T12] intent/term: \"The macro view is the component within the Spec Explorer UI that narrates the f…\" [has detail]\n- [T13] intent/term: \"A phantom node represents the case where no perspective is selected — it appear…\" [has detail]\n\n- Edge #1: CTX8 —[dependency]→ CON7\n- Edge #2: CTX8 —[dependency]→ CON9\n- Edge #3: CTX28 —[dependency]→ CTX17\n- Edge #4: CTX37 —[dependency]→ CTX39\n- Edge #5: CTX8 —[dependency]→ CTX17\n- Edge #6: CTX28 —[dependency]→ CTX8"}],"details":{"nodes":[{"id":6,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":1,"title":"The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…","body":"The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded corners, or blue primary buttons.","basis":"explicit","source":"external-observed [C6]","createdAtLsn":2,"updatedAtLsn":2},{"id":8,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":2,"title":"This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…","body":"This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitly out of scope.","basis":"explicit","source":"stakeholder [C5]","createdAtLsn":2,"updatedAtLsn":2},{"id":11,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":3,"title":"The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.","body":"The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.","basis":"explicit","source":"stakeholder [C10]","createdAtLsn":2,"updatedAtLsn":2},{"id":13,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":4,"title":"Users must manually refresh to see new derivation steps in the macro view.","body":"Users must manually refresh to see new derivation steps in the macro view.","basis":"explicit","source":"stakeholder [C12]","createdAtLsn":2,"updatedAtLsn":2},{"id":19,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":5,"title":"The macro view is built inside a Vite + React + Tailwind SPA.","body":"The macro view is built inside a Vite + React + Tailwind SPA.","basis":"explicit","source":"stakeholder [C2]","createdAtLsn":2,"updatedAtLsn":2},{"id":21,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":6,"title":"Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.","body":"Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.","basis":"explicit","source":"stakeholder [C9]","createdAtLsn":2,"updatedAtLsn":2},{"id":28,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":7,"title":"Phase color values must be expressed as oklch values within the phosphor palette.","body":"Phase color values must be expressed as oklch values within the phosphor palette.","basis":"explicit","source":"stakeholder [C13]","createdAtLsn":2,"updatedAtLsn":2},{"id":38,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":8,"title":"The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…","body":"The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes and vertical flow.","basis":"explicit","source":"stakeholder [C3]","createdAtLsn":2,"updatedAtLsn":2},{"id":50,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":9,"title":"The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…","body":"The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no generic UI patterns.","basis":"explicit","source":"stakeholder [C4]","createdAtLsn":2,"updatedAtLsn":2},{"id":56,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":10,"title":"Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.","body":"Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.","basis":"explicit","source":"stakeholder [C8]","createdAtLsn":2,"updatedAtLsn":2},{"id":64,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":11,"title":"The macro view must use React Flow (@xyflow/react) version ^12.","body":"The macro view must use React Flow (@xyflow/react) version ^12.","basis":"explicit","source":"stakeholder [C1]","createdAtLsn":2,"updatedAtLsn":2},{"id":70,"specId":1,"plane":"intent","kind":"constraint","kindOrdinal":12,"title":"The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.","body":"The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.","basis":"explicit","source":"stakeholder [C11]","createdAtLsn":2,"updatedAtLsn":2},{"id":1,"specId":1,"plane":"intent","kind":"context","kindOrdinal":1,"title":"In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…","body":"In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a separate LLM agent that synthesizes the minimal viable set before reconciliation.","basis":"explicit","source":"external-observed [X7]","createdAtLsn":2,"updatedAtLsn":2},{"id":2,"specId":1,"plane":"intent","kind":"context","kindOrdinal":2,"title":"The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…","body":"The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosphor glow behavior, scanline texture, and JetBrains Mono font.","basis":"explicit","source":"external-observed [X6]","createdAtLsn":2,"updatedAtLsn":2},{"id":4,"specId":1,"plane":"intent","kind":"context","kindOrdinal":3,"title":"Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…","body":"Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor glow, scanline textures).","basis":"explicit","source":"stakeholder [X11]","createdAtLsn":2,"updatedAtLsn":2},{"id":5,"specId":1,"plane":"intent","kind":"context","kindOrdinal":4,"title":"Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.","body":"Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.","basis":"explicit","source":"stakeholder [X29]","createdAtLsn":2,"updatedAtLsn":2},{"id":10,"specId":1,"plane":"intent","kind":"context","kindOrdinal":5,"title":"Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.","body":"Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.","basis":"explicit","source":"stakeholder [X36]","createdAtLsn":2,"updatedAtLsn":2},{"id":14,"specId":1,"plane":"intent","kind":"context","kindOrdinal":6,"title":"Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.","body":"Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.","basis":"explicit","source":"stakeholder [X31]","createdAtLsn":2,"updatedAtLsn":2},{"id":15,"specId":1,"plane":"intent","kind":"context","kindOrdinal":7,"title":"The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.","body":"The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.","basis":"explicit","source":"stakeholder [X2]","createdAtLsn":2,"updatedAtLsn":2},{"id":16,"specId":1,"plane":"intent","kind":"context","kindOrdinal":8,"title":"src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…","body":"src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping (amber), --color-phase-pinning (cyan), --color-phase-defining-done (purple), plus --color-phosphor-red, surface-0..3 warm darks, and --font-mono JetBrains Mono.","basis":"explicit","source":"technical-observed [X39]","createdAtLsn":2,"updatedAtLsn":2},{"id":17,"specId":1,"plane":"intent","kind":"context","kindOrdinal":9,"title":"Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.","body":"Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.","basis":"explicit","source":"stakeholder [X33]","createdAtLsn":2,"updatedAtLsn":2},{"id":18,"specId":1,"plane":"intent","kind":"context","kindOrdinal":10,"title":"Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.","body":"Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.","basis":"explicit","source":"stakeholder [X18]","createdAtLsn":2,"updatedAtLsn":2},{"id":20,"specId":1,"plane":"intent","kind":"context","kindOrdinal":11,"title":"Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…","body":"Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the outcome indicator.","basis":"explicit","source":"stakeholder [X35]","createdAtLsn":2,"updatedAtLsn":2},{"id":26,"specId":1,"plane":"intent","kind":"context","kindOrdinal":12,"title":"Stakeholder preference: a failed run is shown with a red color and a dimmed interior.","body":"Stakeholder preference: a failed run is shown with a red color and a dimmed interior.","basis":"explicit","source":"stakeholder [X27]","createdAtLsn":2,"updatedAtLsn":2},{"id":27,"specId":1,"plane":"intent","kind":"context","kindOrdinal":13,"title":"Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…","body":"Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information density is high but not overwhelming.","basis":"explicit","source":"stakeholder [X37]","createdAtLsn":2,"updatedAtLsn":2},{"id":29,"specId":1,"plane":"intent","kind":"context","kindOrdinal":14,"title":"A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…","body":"A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story IR → Layout → Render with React Flow custom node types; the file is otherwise a stub.","basis":"explicit","source":"technical-observed [X38]","createdAtLsn":2,"updatedAtLsn":2},{"id":30,"specId":1,"plane":"intent","kind":"context","kindOrdinal":15,"title":"The macro view is one specific view within the broader Spec Explorer UI.","body":"The macro view is one specific view within the broader Spec Explorer UI.","basis":"explicit","source":"stakeholder [X1]","createdAtLsn":2,"updatedAtLsn":2},{"id":31,"specId":1,"plane":"intent","kind":"context","kindOrdinal":16,"title":"Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…","body":"Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue border, bail=red border + dimmed interior.","basis":"explicit","source":"stakeholder [X28]","createdAtLsn":2,"updatedAtLsn":2},{"id":32,"specId":1,"plane":"intent","kind":"context","kindOrdinal":17,"title":"Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…","body":"Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/commitment), defining_done=violet (completion/closure).","basis":"explicit","source":"stakeholder [X34]","createdAtLsn":2,"updatedAtLsn":2},{"id":33,"specId":1,"plane":"intent","kind":"context","kindOrdinal":18,"title":"Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.","body":"Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.","basis":"explicit","source":"stakeholder [X26]","createdAtLsn":2,"updatedAtLsn":2},{"id":34,"specId":1,"plane":"intent","kind":"context","kindOrdinal":19,"title":"Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…","body":"Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constructs.","basis":"explicit","source":"stakeholder [X15]","createdAtLsn":2,"updatedAtLsn":2},{"id":35,"specId":1,"plane":"intent","kind":"context","kindOrdinal":20,"title":"The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…","body":"The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than graph content.","basis":"explicit","source":"stakeholder [X3]","createdAtLsn":2,"updatedAtLsn":2},{"id":36,"specId":1,"plane":"intent","kind":"context","kindOrdinal":21,"title":"Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…","body":"Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relationship using lineage edge types.","basis":"explicit","source":"external-observed [X8]","createdAtLsn":2,"updatedAtLsn":2},{"id":37,"specId":1,"plane":"intent","kind":"context","kindOrdinal":22,"title":"Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.","body":"Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.","basis":"explicit","source":"stakeholder [X30]","createdAtLsn":2,"updatedAtLsn":2},{"id":39,"specId":1,"plane":"intent","kind":"context","kindOrdinal":23,"title":"Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…","body":"Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation branch.","basis":"explicit","source":"stakeholder [X16]","createdAtLsn":2,"updatedAtLsn":2},{"id":40,"specId":1,"plane":"intent","kind":"context","kindOrdinal":24,"title":"Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.","body":"Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.","basis":"explicit","source":"stakeholder [X12]","createdAtLsn":2,"updatedAtLsn":2},{"id":41,"specId":1,"plane":"intent","kind":"context","kindOrdinal":25,"title":"Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…","body":"Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected', red for 'impasse_surfaced' — distinguished by a colored left border or chip.","basis":"explicit","source":"stakeholder [X32]","createdAtLsn":2,"updatedAtLsn":2},{"id":42,"specId":1,"plane":"intent","kind":"context","kindOrdinal":26,"title":"Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.","body":"Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.","basis":"explicit","source":"stakeholder [X9]","createdAtLsn":2,"updatedAtLsn":2},{"id":44,"specId":1,"plane":"intent","kind":"context","kindOrdinal":27,"title":"Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…","body":"Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-peel derivation depth.","basis":"explicit","source":"stakeholder [X14]","createdAtLsn":2,"updatedAtLsn":2},{"id":45,"specId":1,"plane":"intent","kind":"context","kindOrdinal":28,"title":"The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…","body":"The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green and pinning=amber. Reconciling this is a minor concern but theme.css is the source of truth for the existing design system.","basis":"explicit","source":"technical-observed [X41]","createdAtLsn":2,"updatedAtLsn":2},{"id":46,"specId":1,"plane":"intent","kind":"context","kindOrdinal":29,"title":"The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…","body":"The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md document.","basis":"explicit","source":"external-observed [X5]","createdAtLsn":2,"updatedAtLsn":2},{"id":47,"specId":1,"plane":"intent","kind":"context","kindOrdinal":30,"title":"Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.","body":"Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.","basis":"explicit","source":"stakeholder [X24]","createdAtLsn":2,"updatedAtLsn":2},{"id":49,"specId":1,"plane":"intent","kind":"context","kindOrdinal":31,"title":"Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.","body":"Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.","basis":"explicit","source":"stakeholder [X10]","createdAtLsn":2,"updatedAtLsn":2},{"id":51,"specId":1,"plane":"intent","kind":"context","kindOrdinal":32,"title":"Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.","body":"Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.","basis":"explicit","source":"stakeholder [X22]","createdAtLsn":2,"updatedAtLsn":2},{"id":52,"specId":1,"plane":"intent","kind":"context","kindOrdinal":33,"title":"Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.","body":"Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.","basis":"explicit","source":"stakeholder [X17]","createdAtLsn":2,"updatedAtLsn":2},{"id":54,"specId":1,"plane":"intent","kind":"context","kindOrdinal":34,"title":"Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…","body":"Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could be materialized later.","basis":"explicit","source":"stakeholder [X23]","createdAtLsn":2,"updatedAtLsn":2},{"id":55,"specId":1,"plane":"intent","kind":"context","kindOrdinal":35,"title":"Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.","body":"Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.","basis":"explicit","source":"stakeholder [X19]","createdAtLsn":2,"updatedAtLsn":2},{"id":59,"specId":1,"plane":"intent","kind":"context","kindOrdinal":36,"title":"Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…","body":"Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING' or '⚡ NUDGE' in phosphor amber) rather than as an external indicator.","basis":"explicit","source":"stakeholder [X25]","createdAtLsn":2,"updatedAtLsn":2},{"id":61,"specId":1,"plane":"intent","kind":"context","kindOrdinal":37,"title":"A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…","body":"A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasible.","basis":"explicit","source":"technical-observed [X40]","createdAtLsn":2,"updatedAtLsn":2},{"id":65,"specId":1,"plane":"intent","kind":"context","kindOrdinal":38,"title":"Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…","body":"Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with mode differences expressed via color, label, or border style only.","basis":"explicit","source":"stakeholder [X20]","createdAtLsn":2,"updatedAtLsn":2},{"id":67,"specId":1,"plane":"intent","kind":"context","kindOrdinal":39,"title":"A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.","body":"A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.","basis":"explicit","source":"stakeholder [X4]","createdAtLsn":2,"updatedAtLsn":2},{"id":68,"specId":1,"plane":"intent","kind":"context","kindOrdinal":40,"title":"Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…","body":"Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, outcome).","basis":"explicit","source":"stakeholder [X21]","createdAtLsn":2,"updatedAtLsn":2},{"id":69,"specId":1,"plane":"intent","kind":"context","kindOrdinal":41,"title":"Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…","body":"Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in, reconciliation, perspective, phantom, and dead-end impasse.","basis":"explicit","source":"stakeholder [X13]","createdAtLsn":2,"updatedAtLsn":2},{"id":43,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":1,"title":"Each node's form and function must visually communicate what happened at that specific point in the derivation tree.","body":"Each node's form and function must visually communicate what happened at that specific point in the derivation tree.","basis":"explicit","source":"stakeholder [G4]","createdAtLsn":2,"updatedAtLsn":2},{"id":57,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":2,"title":"The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.","body":"The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.","basis":"explicit","source":"stakeholder [G2]","createdAtLsn":2,"updatedAtLsn":2},{"id":60,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":3,"title":"A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…","body":"A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without needing to open a detail panel.","basis":"explicit","source":"stakeholder [G3]","createdAtLsn":2,"updatedAtLsn":2},{"id":66,"specId":1,"plane":"intent","kind":"goal","kindOrdinal":4,"title":"The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…","body":"The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivation, reconciliation, and resolution.","basis":"explicit","source":"stakeholder [G1]","createdAtLsn":2,"updatedAtLsn":2},{"id":3,"specId":1,"plane":"intent","kind":"term","kindOrdinal":1,"title":"The spec-elicitation system's derivation process consists of four phases in str…","basis":"explicit","source":"external-observed [T2]","detail":{"definition":"The spec-elicitation system's derivation process consists of four phases in strict dependency order: grounding, shaping, pinning, and defining_done."},"createdAtLsn":2,"updatedAtLsn":2},{"id":7,"specId":1,"plane":"intent","kind":"term","kindOrdinal":2,"title":"The HubNode type has hubType of justification | decision | impasse | perspectiv…","basis":"explicit","source":"technical-observed [T7]","detail":{"definition":"The HubNode type has hubType of justification | decision | impasse | perspective, and carries impasseStatus (open | resolved | superseded) and perspectiveStatus (open | selected | rejected)."},"createdAtLsn":2,"updatedAtLsn":2},{"id":9,"specId":1,"plane":"intent","kind":"term","kindOrdinal":3,"title":"The onion-peel structure refers to the iterative cycle of impasse discovery, re…","basis":"explicit","source":"external-observed [T13]","detail":{"definition":"The onion-peel structure refers to the iterative cycle of impasse discovery, rederivation, fan-out/fan-in synthesis, reconciliation, and resolution that builds the derivation history."},"createdAtLsn":2,"updatedAtLsn":2},{"id":12,"specId":1,"plane":"intent","kind":"term","kindOrdinal":4,"title":"The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…","basis":"explicit","source":"technical-observed [T6]","detail":{"definition":"The ReconciliationRecord type captures reconciliation outcomes (accepted | retry | recurse | bail) along with candidateNodeIds, baselineNodeIds, activatedNodeIds, archivedNodeIds, triggerImpasseIds, resolvedImpasseIds, unresolvedImpasseIds, and a materialProgress flag."},"createdAtLsn":2,"updatedAtLsn":2},{"id":22,"specId":1,"plane":"intent","kind":"term","kindOrdinal":5,"title":"The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…","basis":"explicit","source":"technical-observed [T3]","detail":{"definition":"The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase, triggerImpasseIds, mode (initial | rederive | grounding_enrichment), attemptNumber, nudgingActive, createdAt, and summary."},"createdAtLsn":2,"updatedAtLsn":2},{"id":23,"specId":1,"plane":"intent","kind":"term","kindOrdinal":6,"title":"The ArtifactFile type bundles all spec data into a single file: manifest, sourc…","basis":"explicit","source":"technical-observed [T8]","detail":{"definition":"The ArtifactFile type bundles all spec data into a single file: manifest, sources, extractedClaims, interventions, and a graph object containing nodes, edges, frames, derivationRuns, fanInRecords, reconciliationRecords, and snapshots."},"createdAtLsn":2,"updatedAtLsn":2},{"id":24,"specId":1,"plane":"intent","kind":"term","kindOrdinal":7,"title":"The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…","basis":"explicit","source":"technical-observed [T4]","detail":{"definition":"The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeIds, outputCandidateIds, impassesFound, status (running | completed | failed), and createdAt."},"createdAtLsn":2,"updatedAtLsn":2},{"id":25,"specId":1,"plane":"intent","kind":"term","kindOrdinal":8,"title":"A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…","basis":"explicit","source":"stakeholder [T11]","detail":{"definition":"A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the system gave up on that branch entirely."},"createdAtLsn":2,"updatedAtLsn":2},{"id":48,"specId":1,"plane":"intent","kind":"term","kindOrdinal":9,"title":"The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…","basis":"explicit","source":"technical-observed [T5]","detail":{"definition":"The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds, groupings (with resolution: merged | best_selected | impasse_surfaced), outputCandidateIds, droppedCandidateIds, and createdAt."},"createdAtLsn":2,"updatedAtLsn":2},{"id":53,"specId":1,"plane":"intent","kind":"term","kindOrdinal":10,"title":"materialProgress=true on a ReconciliationRecord means at least some nodes were…","basis":"explicit","source":"stakeholder [T12]","detail":{"definition":"materialProgress=true on a ReconciliationRecord means at least some nodes were activated or archived during that reconciliation — real forward progress occurred."},"createdAtLsn":2,"updatedAtLsn":2},{"id":58,"specId":1,"plane":"intent","kind":"term","kindOrdinal":11,"title":"The Phase type has four ordered values: grounding, shaping, pinning, and defini…","basis":"explicit","source":"technical-observed [T1]","detail":{"definition":"The Phase type has four ordered values: grounding, shaping, pinning, and defining_done."},"createdAtLsn":2,"updatedAtLsn":2},{"id":62,"specId":1,"plane":"intent","kind":"term","kindOrdinal":12,"title":"The macro view is the component within the Spec Explorer UI that narrates the f…","basis":"explicit","source":"stakeholder [T14]","detail":{"definition":"The macro view is the component within the Spec Explorer UI that narrates the full derivation history of a spec via a pannable, zoomable React Flow graph of ~20–40 semantically typed nodes."},"createdAtLsn":2,"updatedAtLsn":2},{"id":63,"specId":1,"plane":"intent","kind":"term","kindOrdinal":13,"title":"A phantom node represents the case where no perspective is selected — it appear…","basis":"explicit","source":"stakeholder [T10]","detail":{"definition":"A phantom node represents the case where no perspective is selected — it appears when an alternative perspective is used."},"createdAtLsn":2,"updatedAtLsn":2}],"edges":[{"id":1,"specId":1,"category":"dependency","sourceId":16,"targetId":28,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":2,"specId":1,"category":"dependency","sourceId":16,"targetId":50,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":3,"specId":1,"category":"dependency","sourceId":45,"targetId":32,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":4,"specId":1,"category":"dependency","sourceId":61,"targetId":67,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":5,"specId":1,"category":"dependency","sourceId":16,"targetId":32,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2},{"id":6,"specId":1,"category":"dependency","sourceId":45,"targetId":16,"basis":"explicit","createdAtLsn":2,"updatedAtLsn":2}],"nodeCount":70,"edgeCount":6,"lsn":2},"isError":false,"timestamp":1780649083224}} +{"type":"message","id":"7226ad3d","parentId":"9048aadd","timestamp":"2026-06-05T08:45:01.565Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Refining launch criteria**\n\nI'm focusing on making sure I commit changes effectively, aiming for a small intent expansion with 1-3 nodes to improve the launch's usefulness. The existing launch doesn't seem effective; maybe it needs a better demo for first-load or error handling. I’m thinking about expanding the intent plane and connecting everything to legal categories and constraints. It may be best to create a requirement that the initial viewport must fit a full derivation, ensuring it’s clear and not duplicative. This way, users see the complete story immediately upon opening.\n\n**Improving canvas launch**\n\nI'm thinking that on the initial load, the canvas should automatically fit the fully expanded derivation graph into view, ensuring there's enough margin for context. This would enhance launch usefulness based on specific constraints like CON6. Also, if the artifact can’t be rendered, it should display a clear diagnostic error instead of just a blank canvas. I'll need to ensure no base nodes are duplicated and explore how the dependencies establish connections to goals and constraints. Finally, it seems we should stop responding after a successful commit.","thinkingSignature":"{\"id\":\"rs_07abc342075b133e016a228c7c6c5c819191399adb02cf55e1\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqIoyK8cC3U6DfQMtM0L9vdVpOOC_0tQ49q8q90NHHxhGCoin6k_1K0BqIHyLg39trQA0GWq4qm6e0WY4uFJpC5nHbIYJNKDCZtw1BvkoNSekxOnba3Bisc-e3eisbh0M1WxcUa9uQzZlodcla89sWA1v54FjbRPsiSDCpX_pnBM5DOtBEybXzauzeBaiNmYCFWawndrg31lSKOFW5JUlTEOdi4P3b7J22K5cLT1Aobef6znxPUnLDx-wBWDtMn2WKv8yukysuoVqdiwHw2K8RwxMtxeoK3UakYS61MMgfbp4qMA19dFGfBTaLYnTJ9iY3ZrFJZ-bZrmEQwQ8SUzXRS6WZxLoWIGqbsHiGX5HAsC0vsxolk1q36SrPqeyS9Do-3duVJ11ErwFzPttyvScLjHSqbQY-JiAU8rhrLGYt5CvHV8pX9EGrsoFwuK5Om_iyDuH-Mcxb0T8AUjbBHRixr1fYw_OjIhn4ChfP5aQ3iHzh6OyPWZWl3Ymm06GpMOaMbCk88wURnBDmTT6nQcM2emmRhOeayWZOy7JlWcMluonmCGxd70Ksts7cKnGPFM3sXWc5Vsnn2KSNxHJb_TIZtjWBCxWBR5xygV5bFTPah8zW9cC7075_KhoQ0u4VhAL3gXVQW-KOPezYJoFP0r9R6q_wlZwvadQsPg7hles8iC1pSRi95x7tREZr49p8mmjZ-Of9ybxjyTQOn9cz63ZtmaWyeQxgUO3v8ZMs6rO57mdrKqS-aF3wKtrh7QMbn_HTYtp5UrSS8H0GWtNUyQQqFabbNBU_S4QSgyf0mHmnkeTpG6wa7IHtBPxb_qotH5rpg2rxOVaHUkgSHH_WRxT7AmPe50k8G2GGyDXN2thXVO4rmirDwFWmQIdhgzVUM_gBtw7zxfZ-MeUnDfCUmLgFs9-n_NmVHXAq1W0HRdaFHNgKzAv6m_FbSTrWmzjOHIqawTinVXNr7NqCoQEZIskl_DCsjSTdCAw7nOFne9Q4uQDrfudTjbCnkmfl-KBBGQx9WJb4tFFp4dRvUEzReCSSCDJe4eVJoGHs-DG33zu4dxkOlPKib_clvRzKY2LR7okxEZD1upkOcp-ANQsLg-nJG07kvgrLelZiW2I9enU2l87PW2142v8myyZB1Zu3-qND3jiHMwNlMJkaCNqJoNJWNEyvZGvdHxZTL0QPbZPqs-incB4sEjM4naMOZaDzRvvNGH78Likw9aRiGv3TbSQRDpIy-SAdfdTI5HMz0LkSCgmHm7-lHcBX2ifpeFvLkavjqHGUa76A3tQDN3dxk5-SpIQQ33jsEM6NX7uqdEJSP0u07EJlI1oMwCN5KIWxMuCMqQsZWGr-IDDIuN3d1nczascMBKs6zANkQQ09m9HBk0ZKAU0-RS6AklvGKWupWjG5WNjEGcfdhhyn3ax_4XT0eMY54d3HbvCMTw78nZlaLAXvBu6VE78hXCz9fqpBexRVws2eLUHh6AUGEzRCNcC7cGo2fZizSq00RYtPYZ_-CHGMdQWByaZBlvApjHJmCrrSqr0vInFBRt-WIEVCRtnLYhEcMyrjlzlDYFelavMbYSaAs8bjCs43oxRhXgqsmlcMDxm5NDQ_jd7Ug-4Re7T-D1Q6GqXOB8lI--WWR2flWLtwxyKmf-yLbjXksLUlE-URoTWyeyFjFyzznB77OElNfukO1Y3JKICPe10WeLRCWVBazlrqY_XtTZyxzCgDf625BU9z4_t6ur-o7Fz2Bcg1RRfS28N6mrRZBN28KY70m2oAK98_FRWs--nrsT_O5VVEGm_UJgzqLhxhmGD3Qi2XGeIJ4NP2DITS9lV6jNk-hVpMHjjHOoqDdWyykABbBLXT0OqdtwTRPC7HTYWmHrNdf7LM5N6yTh7M8SyAgwom_lSduLxwqcVC36enmdGd-RCCTBwE2Rzo7s7RfIVh4L4Fi0UhH4IAjsBAENN3x7Rw41KTyQbU4zNYLoYgCCHBPtrq-ki0SBsQQ50kDMzNODgRYrYrga28Fbapjpool9wZWVq3qutHLvEgMkQb_25WyBBIOOG8jMYuUTshX9JkpK6XIFxBbXEbtcsT9dTbtQqyPcoqXixMaS3cA75iYvlErAXm6tO9hqTC83Vfkn953qSev-sf_4gceJJV9je65NcAPn8AJItQHo7I78sPBORVsavtpw6OkPJYhfNJfG-_TqiazDOILINh_Zkilqtt7EIPuv0ER17-ZZl8CQzddqMOTe1cJc3qA7qaGagP9P5hdoiOQa62Pb2-qAIfuZluf7JzJWvvliWvsDlBuyA9NXslWnEED6AmwKuesGNxGXCTkMoVYxW5y6VUiRdpgElSth98NU8mRV9WJSiy9yYVSRCYF5QXmBlPd_NgnRJW3I3w188i3Y5BZ1JnYFG-krlMBQtKUvWoYEN3DQVYFEhd9IUTylC7ZTorOgSb4EsFLWPHYaZCdk6csnIsFEV2m41ZrjcF4d2E1X8QjSvrV1VDmWju3CVvdouZo1vSKYRvXvckAj1MEbz7I6PmAB-ac3jg3Pifbc3SPhnuvVoW-X5NMKuEdWFiqYENSDEjzkZiEedgG9h4tGBgtOztlE5AQC4YLGaqsGmpdo9ZDcDEGX1oc6hmUhGIahmJdUkK7uuGBcTl-h2_nuNJwFswws74Am72N_UoWAuDDP9lBdKvTvnlD_jERlsxiivckU3qBZwguFGVrgsC4uZo6nBFCG4zoS9KYeiyIawV8CsbJnIdRsa-Q6Lh7W1Hc1_MmYttNgUxvP80enQWP6bXso4_I_KcVQfg_L6PLE_ZjiXSdrcho7w_qLkcgKYA0rMwbd2M3MoMU8xnL4LJdiEbsiL3mJaJZcCDe_UA0GPW22sc33poUEs5lYSr29r1aFidBSKWHDbhLe1L7nkt_5otmc-Xts2FN-vz6b4DvKPf1DpwdD_CXNNBTW_LXzk4DKgF7InRQs4ktR4t-y1KlybzlLlQqmHhNlAC5p6VaCM155CbloAB18x_cDrvVDpO7B55uvBJyIygLtj0XYfFgZ8Ye5ZcWAxLHShCh3t3Ag84IwHL5Jsf_n9nGnJ_TfSl6y5Nv2l7l-7m3FrNZqsl1gS_0YA58BU8IV5zp1ySfuNCFq0jA4PEdj3VSj313uKHWkP_rm3iReu12kdZV_EgRE6lblEX8R-LtAIuGdLCSrWHlHvDS97HN8d32mf712B8o3nwZ4khoMQmcdPbipdctvRK4I2iGvf_oVqYke97I4F0aK5HMJrY5nRLoerqrn8QvTZr9FQztuzcKUc272ICDQKsoKeOqXUyA5Bdkv4V9LRMXh6ysTWEuxI-wGmi6n8SqLSy5lwsUMYHBB6Ks8AsBgvQoRMGDOzLa41JNnvTFrZDTDoS6mspPKbGVQS042K1pe4I0T143qwwbg1AxAx5nP5_4F046ENgg-XZx2DI3xgSIYdirvBQ7S-KTgR4Oub_1OtAxBPwlijxosykGNJeK36A02VGM95nXtsfimsHiQs50joGbiSLe4ErZmJ7tKUVJ0Us-Dpnk9V1kY7PSPS3NCFo2TtTM5KwUePMg5cqA-Tk7OEoDYkhkyzqkcWWrzrrIvDflLpojX_5EsHE0tn5FL5XEwfUbKcukj1T_tspSECLrNvngIcT8GegaRZx-KDwBGsI5lHwJt-8RFL1B0-xO5r3k--1qyQG-qbWIIMIgi--RFQUrFwburqrijMw0riiIIYlsg3ViA74aKYM3wawtu73Jmg-ZY-Os9JjQoePUx2S2eU=\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"**Refining launch criteria**\\n\\nI'm focusing on making sure I commit changes effectively, aiming for a small intent expansion with 1-3 nodes to improve the launch's usefulness. The existing launch doesn't seem effective; maybe it needs a better demo for first-load or error handling. I’m thinking about expanding the intent plane and connecting everything to legal categories and constraints. It may be best to create a requirement that the initial viewport must fit a full derivation, ensuring it’s clear and not duplicative. This way, users see the complete story immediately upon opening.\"},{\"type\":\"summary_text\",\"text\":\"**Improving canvas launch**\\n\\nI'm thinking that on the initial load, the canvas should automatically fit the fully expanded derivation graph into view, ensuring there's enough margin for context. This would enhance launch usefulness based on specific constraints like CON6. Also, if the artifact can’t be rendered, it should display a clear diagnostic error instead of just a blank canvas. I'll need to ensure no base nodes are duplicated and explore how the dependencies establish connections to goals and constraints. Finally, it seems we should stop responding after a successful commit.\"}]}"},{"type":"toolCall","id":"call_3AmhISi57VatkjcR4e1UwfVM|fc_07abc342075b133e016a228c8a712881918f8ae4d8862182b3","name":"commit_graph","arguments":{"nodes":[{"ref":"n1","plane":"intent","kind":"requirement","title":"On initial load, the macro view should fit the fully expanded derivation graph into view.","body":"Because each page load starts fully expanded and the view's launch purpose is to explain how the spec got here, the first rendered viewport should frame the whole derivation story with enough margin that the trunk, phase groups, and nested runs are immediately orientable before the user pans or zooms.","source":"derived from selected spec"},{"ref":"n2","plane":"intent","kind":"requirement","title":"When artifact data is missing or unrendereable, the macro view should show a read-only diagnostic state instead of a blank canvas.","body":"The diagnostic should name the missing or invalid derivation records at a user-facing level while preserving the macro view's read-only, snapshot-only behavior. This keeps first launch useful even when the artifact cannot produce the intended graph.","source":"derived from selected spec"}],"edges":[{"category":"dependency","source":"n1","target":{"existingCode":"CON6"},"rationale":"The viewport requirement is specifically about the existing fully-expanded initial-load behavior."},{"category":"dependency","source":"n1","target":{"existingCode":"CTX31"},"rationale":"The initial framing sits inside the existing pannable, zoomable graph interaction model."},{"category":"support","source":"n1","target":{"existingCode":"G4"},"stance":"for","rationale":"An immediately framed graph helps the user answer how the spec got here before any navigation."},{"category":"dependency","source":"n2","target":{"existingCode":"T6"},"rationale":"The error state is tied to whether the bundled artifact data can produce the macro graph."},{"category":"dependency","source":"n2","target":{"existingCode":"CON3"},"rationale":"The diagnostic must not introduce mutation paths into the read-only macro view."},{"category":"support","source":"n2","target":{"existingCode":"G3"},"stance":"for","rationale":"A named diagnostic is more useful than a blank canvas when node visuals cannot be produced."}]}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.5","usage":{"input":2736,"output":894,"cacheRead":4608,"cacheWrite":0,"totalTokens":8238,"cost":{"input":0.013680000000000001,"output":0.02682,"cacheRead":0.002304,"cacheWrite":0,"total":0.042804}},"stopReason":"toolUse","timestamp":1780649083224,"responseId":"resp_07abc342075b133e016a228c7b8ba08191b97aa384d6a1cff8"}} +{"type":"message","id":"8d679470","parentId":"7226ad3d","timestamp":"2026-06-05T08:45:01.570Z","message":{"role":"toolResult","toolCallId":"call_3AmhISi57VatkjcR4e1UwfVM|fc_07abc342075b133e016a228c8a712881918f8ae4d8862182b3","toolName":"commit_graph","content":[{"type":"text","text":"Graph committed successfully (LSN 3).\nNodes created: n1 → R1, n2 → R2\nEdges created: #7, #8, #9, #10, #11, #12"}],"details":{"status":"success","lsn":3,"createdNodes":{"n1":{"id":71,"code":"R1"},"n2":{"id":72,"code":"R2"}},"edges":[7,8,9,10,11,12]},"isError":false,"timestamp":1780649101570}} +{"type":"message","id":"c382d9d1","parentId":"8d679470","timestamp":"2026-06-05T08:45:02.381Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_07abc342075b133e016a228c8e84108191993ad6f9621b8bd3\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.5","usage":{"input":1125,"output":4,"cacheRead":7168,"cacheWrite":0,"totalTokens":8297,"cost":{"input":0.005625000000000001,"output":0.00012,"cacheRead":0.003584,"cacheWrite":0,"total":0.009329}},"stopReason":"stop","timestamp":1780649101570,"responseId":"resp_07abc342075b133e016a228c8de8a081918496dca22c984e8d"}} +``` diff --git a/.fixtures/seeds/bilal-port-variants/README.md b/.fixtures/seeds/bilal-port-variants/README.md new file mode 100644 index 000000000..5b766d46a --- /dev/null +++ b/.fixtures/seeds/bilal-port-variants/README.md @@ -0,0 +1,20 @@ +# `.fixtures/seeds/bilal-port-variants/` + +Small, reproducible base variants derived from the consolidated Bilal port for product-path fixture curation runs. + +## `macro-view-grounded-intent.json` + +Source: `../bilal-port/macro-view.json`. + +Profile: `grounded-intent`. + +Deterministic filter, implemented in [`_variant-script.ts`](./_variant-script.ts): + +- keep only intent-plane nodes +- keep only `basis: explicit` rows +- keep only nodes whose `source` starts with `stakeholder`, `external-observed`, or `technical-observed` +- keep only edges whose endpoints both survive the node filter +- rewrite `local_id` and edge endpoint ids densely from 1 in source order +- emit spec slug `macro-view-grounded-intent` at `readiness_grade: elicitation_ready` + +The variant is curated starting truth for tracer runs. Product-created curation output is not merged back into this reusable seed; mixed-basis evidence belongs under `.fixtures/runs/fixture-curation//`. diff --git a/.fixtures/seeds/bilal-port-variants/_variant-script.ts b/.fixtures/seeds/bilal-port-variants/_variant-script.ts new file mode 100644 index 000000000..0ad0c3f7b --- /dev/null +++ b/.fixtures/seeds/bilal-port-variants/_variant-script.ts @@ -0,0 +1,83 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +interface SeedFixture { + spec: { + slug: string; + name: string; + readiness_grade: string; + }; + nodes: Array<{ + local_id: number; + plane: string; + kind: string; + title: string; + body?: string | null; + basis?: string | null; + source?: string | null; + detail?: unknown; + }>; + edges: Array<{ + category: string; + source_local_id: number; + target_local_id: number; + stance?: string | null; + basis?: string | null; + rationale?: string | null; + }>; +} + +const VARIANT_SLUG = 'macro-view-grounded-intent'; +const SOURCE_SLUG = 'macro-view'; +const GROUNDED_SOURCE = /^(stakeholder|external-observed|technical-observed)\b/; + +async function main(): Promise { + const here = dirname(fileURLToPath(import.meta.url)); + const sourcePath = join(here, '..', 'bilal-port', `${SOURCE_SLUG}.json`); + const source = JSON.parse(await readFile(sourcePath, 'utf8')) as SeedFixture; + const kept = source.nodes.filter( + (node) => + node.plane === 'intent' && + (node.basis ?? 'explicit') === 'explicit' && + node.source !== null && + node.source !== undefined && + GROUNDED_SOURCE.test(node.source), + ); + const localId = new Map(); + const nodes = kept.map((node, index) => { + const nextId = index + 1; + localId.set(node.local_id, nextId); + return { ...node, local_id: nextId, basis: 'explicit' as const }; + }); + const edges = source.edges.flatMap((edge) => { + const sourceId = localId.get(edge.source_local_id); + const targetId = localId.get(edge.target_local_id); + if (sourceId === undefined || targetId === undefined) return []; + return [ + { + ...edge, + source_local_id: sourceId, + target_local_id: targetId, + basis: 'explicit' as const, + }, + ]; + }); + const variant = { + spec: { + slug: VARIANT_SLUG, + name: 'Macro View — grounded intent base', + readiness_grade: 'elicitation_ready', + }, + nodes, + edges, + } satisfies SeedFixture; + + await mkdir(here, { recursive: true }); + await writeFile(join(here, `${VARIANT_SLUG}.json`), `${JSON.stringify(variant, null, 2)}\n`, 'utf8'); +} + +main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/.fixtures/seeds/bilal-port-variants/macro-view-grounded-intent.json b/.fixtures/seeds/bilal-port-variants/macro-view-grounded-intent.json new file mode 100644 index 000000000..8a6f73ab5 --- /dev/null +++ b/.fixtures/seeds/bilal-port-variants/macro-view-grounded-intent.json @@ -0,0 +1,785 @@ +{ + "spec": { + "slug": "macro-view-grounded-intent", + "name": "Macro View — grounded intent base", + "readiness_grade": "elicitation_ready" + }, + "nodes": [ + { + "local_id": 1, + "plane": "intent", + "kind": "context", + "title": "In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a…", + "body": "In the spec-elicitation system, fan-out runs 2–3 independent clean-room derivation sessions from the same upstream context, and fan-in is a separate LLM agent that synthesizes the minimal viable set before reconciliation.", + "basis": "explicit", + "source": "external-observed [X7]", + "detail": null + }, + { + "local_id": 2, + "plane": "intent", + "kind": "context", + "title": "The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosp…", + "body": "The CRT/phosphor design language is a project-wide standard defined in .impeccable.md, covering amber primary color, dark-only theme, phosphor glow behavior, scanline texture, and JetBrains Mono font.", + "basis": "explicit", + "source": "external-observed [X6]", + "detail": null + }, + { + "local_id": 3, + "plane": "intent", + "kind": "term", + "title": "The spec-elicitation system's derivation process consists of four phases in str…", + "body": null, + "basis": "explicit", + "source": "external-observed [T2]", + "detail": { + "definition": "The spec-elicitation system's derivation process consists of four phases in strict dependency order: grounding, shaping, pinning, and defining_done." + } + }, + { + "local_id": 4, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor…", + "body": "Stakeholder preference: the macro view shares the CRT phosphor design language (amber primary, JetBrains Mono, dark warm surfaces, phosphor glow, scanline textures).", + "basis": "explicit", + "source": "stakeholder [X11]", + "detail": null + }, + { + "local_id": 5, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.", + "body": "Stakeholder preference: bail reconciliation outcome visual treatment intentionally matches the failed run treatment for consistency.", + "basis": "explicit", + "source": "stakeholder [X29]", + "detail": null + }, + { + "local_id": 6, + "plane": "intent", + "kind": "constraint", + "title": "The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded…", + "body": "The design language explicitly prohibits generic UI patterns such as Material Design, Tailwind defaults, SaaS dashboard aesthetics, rounded corners, or blue primary buttons.", + "basis": "explicit", + "source": "external-observed [C6]", + "detail": null + }, + { + "local_id": 7, + "plane": "intent", + "kind": "term", + "title": "The HubNode type has hubType of justification | decision | impasse | perspectiv…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T7]", + "detail": { + "definition": "The HubNode type has hubType of justification | decision | impasse | perspective, and carries impasseStatus (open | resolved | superseded) and perspectiveStatus (open | selected | rejected)." + } + }, + { + "local_id": 8, + "plane": "intent", + "kind": "constraint", + "title": "This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitl…", + "body": "This brief is scoped exclusively to the macro view component and its rendering of the derivation story; other parts of the UI are explicitly out of scope.", + "basis": "explicit", + "source": "stakeholder [C5]", + "detail": null + }, + { + "local_id": 9, + "plane": "intent", + "kind": "term", + "title": "The onion-peel structure refers to the iterative cycle of impasse discovery, re…", + "body": null, + "basis": "explicit", + "source": "external-observed [T13]", + "detail": { + "definition": "The onion-peel structure refers to the iterative cycle of impasse discovery, rederivation, fan-out/fan-in synthesis, reconciliation, and resolution that builds the derivation history." + } + }, + { + "local_id": 10, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.", + "body": "Stakeholder preference: a ReconciliationRecord with outcome='bail' is the mechanism by which a branch becomes a dead-end impasse.", + "basis": "explicit", + "source": "stakeholder [X36]", + "detail": null + }, + { + "local_id": 11, + "plane": "intent", + "kind": "constraint", + "title": "The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.", + "body": "The macro view is strictly read-only; users can pan, zoom, and collapse/expand nodes, but cannot trigger any data mutations.", + "basis": "explicit", + "source": "stakeholder [C10]", + "detail": null + }, + { + "local_id": 12, + "plane": "intent", + "kind": "term", + "title": "The ReconciliationRecord type captures reconciliation outcomes (accepted | retr…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T6]", + "detail": { + "definition": "The ReconciliationRecord type captures reconciliation outcomes (accepted | retry | recurse | bail) along with candidateNodeIds, baselineNodeIds, activatedNodeIds, archivedNodeIds, triggerImpasseIds, resolvedImpasseIds, unresolvedImpasseIds, and a materialProgress flag." + } + }, + { + "local_id": 13, + "plane": "intent", + "kind": "constraint", + "title": "Users must manually refresh to see new derivation steps in the macro view.", + "body": "Users must manually refresh to see new derivation steps in the macro view.", + "basis": "explicit", + "source": "stakeholder [C12]", + "detail": null + }, + { + "local_id": 14, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.", + "body": "Stakeholder preference: lane widths in the macro layout scale proportionally to the number of nodes at that depth rather than being fixed.", + "basis": "explicit", + "source": "stakeholder [X31]", + "detail": null + }, + { + "local_id": 15, + "plane": "intent", + "kind": "context", + "title": "The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.", + "body": "The existing micro view is built with Sigma.js, handles 700+ nodes, and answers 'what does the graph look like now?'.", + "basis": "explicit", + "source": "stakeholder [X2]", + "detail": null + }, + { + "local_id": 16, + "plane": "intent", + "kind": "context", + "title": "src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping…", + "body": "src/styles/theme.css already defines the phosphor palette as oklch tokens including --color-phase-grounding (green), --color-phase-shaping (amber), --color-phase-pinning (cyan), --color-phase-defining-done (purple), plus --color-phosphor-red, surface-0..3 warm darks, and --font-mono JetBrains Mono.", + "basis": "explicit", + "source": "technical-observed [X39]", + "detail": null + }, + { + "local_id": 17, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.", + "body": "Stakeholder preference: clicking a node opens a detail panel on the right side of the screen.", + "basis": "explicit", + "source": "stakeholder [X33]", + "detail": null + }, + { + "local_id": 18, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.", + "body": "Stakeholder preference: React Flow group/parent nodes are used for phase containers in the macro view.", + "basis": "explicit", + "source": "stakeholder [X18]", + "detail": null + }, + { + "local_id": 19, + "plane": "intent", + "kind": "constraint", + "title": "The macro view is built inside a Vite + React + Tailwind SPA.", + "body": "The macro view is built inside a Vite + React + Tailwind SPA.", + "basis": "explicit", + "source": "stakeholder [C2]", + "detail": null + }, + { + "local_id": 20, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the…", + "body": "Stakeholder preference: the materialProgress flag is visualized as a subtle chip or checkmark inside the reconciliation node, alongside the outcome indicator.", + "basis": "explicit", + "source": "stakeholder [X35]", + "detail": null + }, + { + "local_id": 21, + "plane": "intent", + "kind": "constraint", + "title": "Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.", + "body": "Every page load of the macro view starts fully expanded, with no memory of previously collapsed nodes.", + "basis": "explicit", + "source": "stakeholder [C9]", + "detail": null + }, + { + "local_id": 22, + "plane": "intent", + "kind": "term", + "title": "The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase,…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T3]", + "detail": { + "definition": "The FrameRecord type includes: id, parentFrameId, baselineFrameId, entryPhase, triggerImpasseIds, mode (initial | rederive | grounding_enrichment), attemptNumber, nudgingActive, createdAt, and summary." + } + }, + { + "local_id": 23, + "plane": "intent", + "kind": "term", + "title": "The ArtifactFile type bundles all spec data into a single file: manifest, sourc…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T8]", + "detail": { + "definition": "The ArtifactFile type bundles all spec data into a single file: manifest, sources, extractedClaims, interventions, and a graph object containing nodes, edges, frames, derivationRuns, fanInRecords, reconciliationRecords, and snapshots." + } + }, + { + "local_id": 24, + "plane": "intent", + "kind": "term", + "title": "The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeI…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T4]", + "detail": { + "definition": "The DerivationRunRecord type includes: id, frameId, phase, runIndex, inputNodeIds, outputCandidateIds, impassesFound, status (running | completed | failed), and createdAt." + } + }, + { + "local_id": 25, + "plane": "intent", + "kind": "term", + "title": "A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the s…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T11]", + "detail": { + "definition": "A dead-end impasse is one whose reconciliation chose the 'bail' outcome — the system gave up on that branch entirely." + } + }, + { + "local_id": 26, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a failed run is shown with a red color and a dimmed interior.", + "body": "Stakeholder preference: a failed run is shown with a red color and a dimmed interior.", + "basis": "explicit", + "source": "stakeholder [X27]", + "detail": null + }, + { + "local_id": 27, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information dens…", + "body": "Stakeholder preference: the visual design must feel like CRT/terminal aesthetic while preserving modern design principles; information density is high but not overwhelming.", + "basis": "explicit", + "source": "stakeholder [X37]", + "detail": null + }, + { + "local_id": 28, + "plane": "intent", + "kind": "constraint", + "title": "Phase color values must be expressed as oklch values within the phosphor palette.", + "body": "Phase color values must be expressed as oklch values within the phosphor palette.", + "basis": "explicit", + "source": "stakeholder [C13]", + "detail": null + }, + { + "local_id": 29, + "plane": "intent", + "kind": "context", + "title": "A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story…", + "body": "A MacroView.tsx component already exists at src/components/MacroView.tsx with a header docblock describing the intended architecture: Story IR → Layout → Render with React Flow custom node types; the file is otherwise a stub.", + "basis": "explicit", + "source": "technical-observed [X38]", + "detail": null + }, + { + "local_id": 30, + "plane": "intent", + "kind": "context", + "title": "The macro view is one specific view within the broader Spec Explorer UI.", + "body": "The macro view is one specific view within the broader Spec Explorer UI.", + "basis": "explicit", + "source": "stakeholder [X1]", + "detail": null + }, + { + "local_id": 31, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue bo…", + "body": "Stakeholder preference: reconciliation outcome affects the whole node's visual weight: accepted=normal, retry=amber border, recurse=blue border, bail=red border + dimmed interior.", + "basis": "explicit", + "source": "stakeholder [X28]", + "detail": null + }, + { + "local_id": 32, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/…", + "body": "Stakeholder preference: phase color assignments are grounding=blue (foundational), shaping=green (growth/emergence), pinning=amber (fixity/commitment), defining_done=violet (completion/closure).", + "basis": "explicit", + "source": "stakeholder [X34]", + "detail": null + }, + { + "local_id": 33, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.", + "body": "Stakeholder preference: a 'running' status is unlikely to appear but should be highlighted somehow if it does.", + "basis": "explicit", + "source": "stakeholder [X26]", + "detail": null + }, + { + "local_id": 34, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constru…", + "body": "Stakeholder preference: derivation phases (such as reconciliation) are encoded as nodes within the graph rather than as separate UI constructs.", + "basis": "explicit", + "source": "stakeholder [X15]", + "detail": null + }, + { + "local_id": 35, + "plane": "intent", + "kind": "context", + "title": "The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than gr…", + "body": "The macro view is structurally different from the micro view: it shows ~20-40 rich nodes representing the derivation process rather than graph content.", + "basis": "explicit", + "source": "stakeholder [X3]", + "detail": null + }, + { + "local_id": 36, + "plane": "intent", + "kind": "context", + "title": "Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relati…", + "body": "Reconciliation compares candidate nodes from re-derivation against the previous iteration's nodes for that phase and classifies each relationship using lineage edge types.", + "basis": "explicit", + "source": "external-observed [X8]", + "detail": null + }, + { + "local_id": 37, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.", + "body": "Stakeholder preference: derivation depth in the macro layout is computed from the parentFrameId chain length.", + "basis": "explicit", + "source": "stakeholder [X30]", + "detail": null + }, + { + "local_id": 38, + "plane": "intent", + "kind": "constraint", + "title": "The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes…", + "body": "The macro view must use a manual layout algorithm rather than dagre, because the spatial grammar requires precise control over depth lanes and vertical flow.", + "basis": "explicit", + "source": "stakeholder [C3]", + "detail": null + }, + { + "local_id": 39, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation…", + "body": "Stakeholder preference: impasse nodes connect sub-trees and increase the breadth of the layout to represent the opening of a new derivation branch.", + "basis": "explicit", + "source": "stakeholder [X16]", + "detail": null + }, + { + "local_id": 40, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.", + "body": "Stakeholder preference: users can fold (collapse) any nested derivation run to hide detail.", + "basis": "explicit", + "source": "stakeholder [X12]", + "detail": null + }, + { + "local_id": 41, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected',…", + "body": "Stakeholder preference: fan-in groupings render as color-coded rows inside the fan-in node — green for 'merged', amber for 'best_selected', red for 'impasse_surfaced' — distinguished by a colored left border or chip.", + "basis": "explicit", + "source": "stakeholder [X32]", + "detail": null + }, + { + "local_id": 42, + "plane": "intent", + "kind": "context", + "title": "Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.", + "body": "Because the macro view renders inside a React Flow canvas (not directly in the DOM), large node counts are not a DOM performance concern.", + "basis": "explicit", + "source": "stakeholder [X9]", + "detail": null + }, + { + "local_id": 43, + "plane": "intent", + "kind": "goal", + "title": "Each node's form and function must visually communicate what happened at that specific point in the derivation tree.", + "body": "Each node's form and function must visually communicate what happened at that specific point in the derivation tree.", + "basis": "explicit", + "source": "stakeholder [G4]", + "detail": null + }, + { + "local_id": 44, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-pe…", + "body": "Stakeholder preference: layout grammar encodes timeline through verticality (t+1 / more recent appears higher) and breadth encodes onion-peel derivation depth.", + "basis": "explicit", + "source": "stakeholder [X14]", + "detail": null + }, + { + "local_id": 45, + "plane": "intent", + "kind": "context", + "title": "The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green…", + "body": "The theme.css token assignment differs from grounding X34: theme.css maps shaping=amber and pinning=cyan, while X34 specifies shaping=green and pinning=amber. Reconciling this is a minor concern but theme.css is the source of truth for the existing design system.", + "basis": "explicit", + "source": "technical-observed [X41]", + "detail": null + }, + { + "local_id": 46, + "plane": "intent", + "kind": "context", + "title": "The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md d…", + "body": "The onion-peel derivation structure (impasse discovery, rederivation, reconciliation, resolution) is specified in the spec-elicitation.md document.", + "basis": "explicit", + "source": "external-observed [X5]", + "detail": null + }, + { + "local_id": 47, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.", + "body": "Stakeholder preference: phantom nodes and faded nodes are informational only and carry no interactive actions.", + "basis": "explicit", + "source": "stakeholder [X24]", + "detail": null + }, + { + "local_id": 48, + "plane": "intent", + "kind": "term", + "title": "The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds,…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T5]", + "detail": { + "definition": "The FanInRecord type captures the fan-in step: id, frameId, phase, inputRunIds, groupings (with resolution: merged | best_selected | impasse_surfaced), outputCandidateIds, droppedCandidateIds, and createdAt." + } + }, + { + "local_id": 49, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.", + "body": "Stakeholder preference: render the derivation narrative as a pannable, zoomable graph.", + "basis": "explicit", + "source": "stakeholder [X10]", + "detail": null + }, + { + "local_id": 50, + "plane": "intent", + "kind": "constraint", + "title": "The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no…", + "body": "The CRT design language for the macro view mandates: var(--font-mono), oklch phase colors, warm-tinted dark surfaces, phosphor glow, and no generic UI patterns.", + "basis": "explicit", + "source": "stakeholder [C4]", + "detail": null + }, + { + "local_id": 51, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.", + "body": "Stakeholder preference: when a nested run is collapsed, sibling nodes shift to fill vacated space, triggering a layout reflow.", + "basis": "explicit", + "source": "stakeholder [X22]", + "detail": null + }, + { + "local_id": 52, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.", + "body": "Stakeholder preference: fan-in and fan-out steps are represented as nested nodes within any one phase node.", + "basis": "explicit", + "source": "stakeholder [X17]", + "detail": null + }, + { + "local_id": 53, + "plane": "intent", + "kind": "term", + "title": "materialProgress=true on a ReconciliationRecord means at least some nodes were…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T12]", + "detail": { + "definition": "materialProgress=true on a ReconciliationRecord means at least some nodes were activated or archived during that reconciliation — real forward progress occurred." + } + }, + { + "local_id": 54, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could…", + "body": "Stakeholder preference: perspective nodes show which branch was taken; unchosen branches fade out to indicate they were not taken but could be materialized later.", + "basis": "explicit", + "source": "stakeholder [X23]", + "detail": null + }, + { + "local_id": 55, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.", + "body": "Stakeholder preference: there is no separate 'trunk' node type; the trunk is simply the outermost shell of the onion-layered layout.", + "basis": "explicit", + "source": "stakeholder [X19]", + "detail": null + }, + { + "local_id": 56, + "plane": "intent", + "kind": "constraint", + "title": "Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.", + "body": "Collapsed/expanded state in the macro view is purely ephemeral in-memory React state and is never persisted.", + "basis": "explicit", + "source": "stakeholder [C8]", + "detail": null + }, + { + "local_id": 57, + "plane": "intent", + "kind": "goal", + "title": "The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.", + "body": "The macro view's visual design must be beautiful, novel, and information-dense, avoiding generic dashboard or AI-generated aesthetics.", + "basis": "explicit", + "source": "stakeholder [G2]", + "detail": null + }, + { + "local_id": 58, + "plane": "intent", + "kind": "term", + "title": "The Phase type has four ordered values: grounding, shaping, pinning, and defini…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T1]", + "detail": { + "definition": "The Phase type has four ordered values: grounding, shaping, pinning, and defining_done." + } + }, + { + "local_id": 59, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING…", + "body": "Stakeholder preference: the nudging state is indicated by a treatment inside the node body (compact labelled chip or badge such as 'NUDGING' or '⚡ NUDGE' in phosphor amber) rather than as an external indicator.", + "basis": "explicit", + "source": "stakeholder [X25]", + "detail": null + }, + { + "local_id": 60, + "plane": "intent", + "kind": "goal", + "title": "A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without need…", + "body": "A user should be able to understand what happened at each derivation step from the node visuals alone (numbers, IDs, outcomes) without needing to open a detail panel.", + "basis": "explicit", + "source": "stakeholder [G3]", + "detail": null + }, + { + "local_id": 61, + "plane": "intent", + "kind": "context", + "title": "A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasib…", + "body": "A DetailPanel.tsx component exists at src/components/DetailPanel.tsx as a sibling to MacroView.tsx, confirming reuse is structurally feasible.", + "basis": "explicit", + "source": "technical-observed [X40]", + "detail": null + }, + { + "local_id": 62, + "plane": "intent", + "kind": "term", + "title": "The macro view is the component within the Spec Explorer UI that narrates the f…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T14]", + "detail": { + "definition": "The macro view is the component within the Spec Explorer UI that narrates the full derivation history of a spec via a pannable, zoomable React Flow graph of ~20–40 semantically typed nodes." + } + }, + { + "local_id": 63, + "plane": "intent", + "kind": "term", + "title": "A phantom node represents the case where no perspective is selected — it appear…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T10]", + "detail": { + "definition": "A phantom node represents the case where no perspective is selected — it appears when an alternative perspective is used." + } + }, + { + "local_id": 64, + "plane": "intent", + "kind": "constraint", + "title": "The macro view must use React Flow (@xyflow/react) version ^12.", + "body": "The macro view must use React Flow (@xyflow/react) version ^12.", + "basis": "explicit", + "source": "stakeholder [C1]", + "detail": null + }, + { + "local_id": 65, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with…", + "body": "Stakeholder preference: all three frame modes (initial, rederive, grounding_enrichment) render as the same phase-group node component, with mode differences expressed via color, label, or border style only.", + "basis": "explicit", + "source": "stakeholder [X20]", + "detail": null + }, + { + "local_id": 66, + "plane": "intent", + "kind": "goal", + "title": "The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivatio…", + "body": "The macro view answers the question 'how did the spec get here?' by visualizing the full onion-peel cycle of impasse discovery, rederivation, reconciliation, and resolution.", + "basis": "explicit", + "source": "stakeholder [G1]", + "detail": null + }, + { + "local_id": 67, + "plane": "intent", + "kind": "context", + "title": "A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.", + "body": "A right-side detail panel is already implemented and wired in the Micro View; the macro view should reuse it.", + "basis": "explicit", + "source": "stakeholder [X4]", + "detail": null + }, + { + "local_id": 68, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, ou…", + "body": "Stakeholder preference: when a nested derivation run is collapsed it shrinks to a small pill or badge node showing key stats (run count, outcome).", + "basis": "explicit", + "source": "stakeholder [X21]", + "detail": null + }, + { + "local_id": 69, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in,…", + "body": "Stakeholder preference: custom React Flow node types exist for each semantic role: trunk phase, impasse, phase group, fan-out, run, fan-in, reconciliation, perspective, phantom, and dead-end impasse.", + "basis": "explicit", + "source": "stakeholder [X13]", + "detail": null + }, + { + "local_id": 70, + "plane": "intent", + "kind": "constraint", + "title": "The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.", + "body": "The macro view is a snapshot-only view: it reads artifact data once on mount and does not update reactively.", + "basis": "explicit", + "source": "stakeholder [C11]", + "detail": null + } + ], + "edges": [ + { + "category": "dependency", + "source_local_id": 16, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 16, + "target_local_id": 50, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 45, + "target_local_id": 32, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 61, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 16, + "target_local_id": 32, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 45, + "target_local_id": 16, + "stance": null, + "basis": "explicit", + "rationale": null + } + ] +} diff --git a/.fixtures/seeds/bilal-port/README.md b/.fixtures/seeds/bilal-port/README.md index 66b535209..f6ee62c0f 100644 --- a/.fixtures/seeds/bilal-port/README.md +++ b/.fixtures/seeds/bilal-port/README.md @@ -70,8 +70,8 @@ columns stay coherent under brunch's mutation contract. ## Stats -| Spec | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | -| code-health | 335 | 600 | 277 | 520 | 117 | 1 | 0 | -| explorer-ui | 316 | 698 | 280 | 614 | 74 | 15 | 0 | -| macro-view | 265 | 568 | 232 | 504 | 68 | 0 | 0 | +| Spec | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | duplicate-after-collapse drops | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| code-health | 335 | 600 | 277 | 446 | 117 | 1 | 0 | 74 | +| explorer-ui | 316 | 698 | 280 | 580 | 74 | 15 | 0 | 34 | +| macro-view | 265 | 568 | 232 | 461 | 68 | 0 | 0 | 43 | diff --git a/.fixtures/seeds/bilal-port/_port-script.ts b/.fixtures/seeds/bilal-port/_port-script.ts index 61cd15d41..8155637c0 100644 --- a/.fixtures/seeds/bilal-port/_port-script.ts +++ b/.fixtures/seeds/bilal-port/_port-script.ts @@ -612,6 +612,7 @@ function portSpec(sourceName: string, slug: string, displayName: string): SpecPo // Phase 3: emit brunch edges const brunchEdges: BrunchEdgeFixture[] = []; + const emittedEdgeKeys = new Set(); const stats = { nodes_in: nodes.length, edges_in: edges.length, @@ -620,6 +621,17 @@ function portSpec(sourceName: string, slug: string, displayName: string): SpecPo edges_absorbed: 0, edges_dropped_self_after_collapse: 0, edges_dropped_unresolved_endpoint: 0, + edges_dropped_duplicate_after_collapse: 0, + }; + + const emitEdge = (edge: BrunchEdgeFixture): void => { + const key = `${edge.source_local_id}\0${edge.target_local_id}\0${edge.category}\0${edge.stance ?? ''}`; + if (emittedEdgeKeys.has(key)) { + stats.edges_dropped_duplicate_after_collapse++; + return; + } + emittedEdgeKeys.add(key); + brunchEdges.push(edge); }; // 3a. synthesize one realization edge per evidence node, from the audit check @@ -628,7 +640,7 @@ function portSpec(sourceName: string, slug: string, displayName: string): SpecPo if (node.kind !== 'content' || node.semanticRole !== 'evidence') continue; const evidenceLocalId = bilalUuidToLocalId.get(node.id); if (evidenceLocalId === undefined) continue; - brunchEdges.push({ + emitEdge({ category: 'realization', source_local_id: auditCheckLocalId, target_local_id: evidenceLocalId, @@ -665,7 +677,7 @@ function portSpec(sourceName: string, slug: string, displayName: string): SpecPo stats.edges_absorbed++; continue; } - brunchEdges.push({ + emitEdge({ category: mapping.category, source_local_id: sourceLocalId, target_local_id: targetLocalId, @@ -783,13 +795,13 @@ function writeReadme(results: { slug: string; displayName: string; stats: Record '', '## Stats', '', - '| Spec | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops |', - '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', + '| Spec | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | duplicate-after-collapse drops |', + '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', ]; for (const r of results) { const s = r.stats; lines.push( - `| ${r.slug} | ${s.nodes_in} | ${s.edges_in} | ${s.nodes_emitted} | ${s.edges_emitted} | ${s.edges_absorbed} | ${s.edges_dropped_self_after_collapse} | ${s.edges_dropped_unresolved_endpoint} |`, + `| ${r.slug} | ${s.nodes_in} | ${s.edges_in} | ${s.nodes_emitted} | ${s.edges_emitted} | ${s.edges_absorbed} | ${s.edges_dropped_self_after_collapse} | ${s.edges_dropped_unresolved_endpoint} | ${s.edges_dropped_duplicate_after_collapse} |`, ); } lines.push(''); diff --git a/.fixtures/seeds/bilal-port/code-health.json b/.fixtures/seeds/bilal-port/code-health.json index 3fd7648ba..139f60f98 100644 --- a/.fixtures/seeds/bilal-port/code-health.json +++ b/.fixtures/seeds/bilal-port/code-health.json @@ -3695,14 +3695,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 255, - "target_local_id": 158, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 134, @@ -4183,14 +4175,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 201, - "target_local_id": 273, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 247, @@ -4215,14 +4199,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 51, - "target_local_id": 266, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 268, @@ -4263,14 +4239,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 267, - "target_local_id": 146, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 265, @@ -4327,14 +4295,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 274, - "target_local_id": 66, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 273, @@ -4415,14 +4375,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 244, - "target_local_id": 276, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 195, @@ -4439,14 +4391,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 79, - "target_local_id": 277, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 268, @@ -4503,14 +4447,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 230, - "target_local_id": 256, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 119, @@ -4551,14 +4487,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 266, - "target_local_id": 151, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 272, @@ -4671,14 +4599,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 227, - "target_local_id": 267, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 92, @@ -4839,14 +4759,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 277, - "target_local_id": 187, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 178, @@ -4943,14 +4855,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 275, - "target_local_id": 15, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 138, @@ -5047,14 +4951,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 259, - "target_local_id": 126, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 82, @@ -5063,14 +4959,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 264, - "target_local_id": 250, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 265, @@ -5095,22 +4983,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 174, - "target_local_id": 264, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 75, - "target_local_id": 270, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 92, @@ -5135,14 +5007,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 272, - "target_local_id": 216, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 265, @@ -5183,14 +5047,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 256, - "target_local_id": 132, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 262, @@ -5311,14 +5167,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 44, - "target_local_id": 272, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 143, @@ -5343,14 +5191,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 70, - "target_local_id": 255, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 179, @@ -5455,22 +5295,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 261, - "target_local_id": 102, - "stance": "for", - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 147, - "target_local_id": 255, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 201, @@ -5543,14 +5367,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 116, - "target_local_id": 272, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 219, @@ -5663,14 +5479,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 259, - "target_local_id": 215, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 262, @@ -5695,14 +5503,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 274, - "target_local_id": 57, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 56, @@ -5711,14 +5511,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 259, - "target_local_id": 215, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 138, @@ -5727,14 +5519,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 241, - "target_local_id": 266, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 217, @@ -5743,14 +5527,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 189, - "target_local_id": 268, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 108, @@ -5783,22 +5559,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 143, - "target_local_id": 276, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 23, - "target_local_id": 277, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 217, @@ -5871,14 +5631,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 257, - "target_local_id": 49, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 76, @@ -5913,32 +5665,16 @@ }, { "category": "dependency", - "source_local_id": 258, - "target_local_id": 15, + "source_local_id": 217, + "target_local_id": 103, "stance": null, "basis": "explicit", "rationale": null }, { "category": "dependency", - "source_local_id": 217, - "target_local_id": 103, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "support", - "source_local_id": 269, - "target_local_id": 28, - "stance": "for", - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 260, - "target_local_id": 20, + "source_local_id": 260, + "target_local_id": 20, "stance": null, "basis": "explicit", "rationale": null @@ -5991,14 +5727,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 184, - "target_local_id": 256, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 269, @@ -6047,14 +5775,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 190, - "target_local_id": 266, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 265, @@ -6063,14 +5783,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 9, - "target_local_id": 263, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 160, @@ -6095,22 +5807,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 270, - "target_local_id": 248, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 167, - "target_local_id": 259, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 268, @@ -6167,14 +5863,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 233, - "target_local_id": 263, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 276, @@ -6191,14 +5879,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 217, - "target_local_id": 273, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 10, @@ -6223,14 +5903,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 176, - "target_local_id": 263, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 194, @@ -6279,14 +5951,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 231, - "target_local_id": 268, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 259, @@ -6391,14 +6055,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 271, - "target_local_id": 164, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 76, @@ -6423,14 +6079,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 272, - "target_local_id": 115, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 48, @@ -6527,14 +6175,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 64, - "target_local_id": 265, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 272, @@ -6575,14 +6215,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 127, - "target_local_id": 271, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 167, @@ -6591,14 +6223,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 268, - "target_local_id": 41, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 241, @@ -6607,14 +6231,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 274, - "target_local_id": 66, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 174, @@ -6647,22 +6263,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 271, - "target_local_id": 117, - "stance": "for", - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 203, - "target_local_id": 265, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 67, @@ -6671,14 +6271,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 276, - "target_local_id": 40, - "stance": "for", - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 201, @@ -6759,22 +6351,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 257, - "target_local_id": 49, - "stance": "for", - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 110, - "target_local_id": 262, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 210, @@ -6791,22 +6367,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 266, - "target_local_id": 141, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 274, - "target_local_id": 120, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 194, @@ -6815,14 +6375,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 91, - "target_local_id": 276, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 82, @@ -6847,14 +6399,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 263, - "target_local_id": 193, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 265, @@ -6895,14 +6439,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 108, - "target_local_id": 255, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 122, @@ -6927,30 +6463,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "support", - "source_local_id": 261, - "target_local_id": 102, - "stance": "for", - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 276, - "target_local_id": 239, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 122, - "target_local_id": 262, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 274, @@ -6975,54 +6487,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 92, - "target_local_id": 264, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 159, - "target_local_id": 255, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 273, - "target_local_id": 235, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 62, - "target_local_id": 264, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 277, - "target_local_id": 187, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 208, - "target_local_id": 275, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 170, @@ -7031,22 +6495,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 160, - "target_local_id": 271, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 27, - "target_local_id": 259, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 276, @@ -7071,22 +6519,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 265, - "target_local_id": 171, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 42, - "target_local_id": 271, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 42, @@ -7095,22 +6527,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 84, - "target_local_id": 268, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 263, - "target_local_id": 193, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 268, @@ -7118,14 +6534,6 @@ "stance": null, "basis": "explicit", "rationale": null - }, - { - "category": "support", - "source_local_id": 260, - "target_local_id": 254, - "stance": "for", - "basis": "explicit", - "rationale": null } ] } diff --git a/.fixtures/seeds/bilal-port/explorer-ui.json b/.fixtures/seeds/bilal-port/explorer-ui.json index 583e3151c..5fd7aaf45 100644 --- a/.fixtures/seeds/bilal-port/explorer-ui.json +++ b/.fixtures/seeds/bilal-port/explorer-ui.json @@ -3470,14 +3470,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 276, - "target_local_id": 100, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 231, @@ -4102,14 +4094,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 269, - "target_local_id": 131, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 81, @@ -4118,14 +4102,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 22, - "target_local_id": 266, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 207, @@ -4814,14 +4790,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 271, - "target_local_id": 255, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 45, @@ -4998,14 +4966,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 245, - "target_local_id": 271, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 108, @@ -5166,14 +5126,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 269, - "target_local_id": 131, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 221, @@ -5374,14 +5326,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 205, - "target_local_id": 274, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 30, @@ -5510,14 +5454,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 262, - "target_local_id": 55, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 72, @@ -5654,14 +5590,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 152, - "target_local_id": 275, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 93, @@ -5910,14 +5838,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 106, - "target_local_id": 273, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 88, @@ -6102,14 +6022,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 210, - "target_local_id": 272, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 201, @@ -6118,14 +6030,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 262, - "target_local_id": 53, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 15, @@ -6278,14 +6182,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 264, - "target_local_id": 84, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 198, @@ -6302,22 +6198,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 265, - "target_local_id": 159, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 59, - "target_local_id": 269, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 37, @@ -6446,14 +6326,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 185, - "target_local_id": 262, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 262, @@ -6534,14 +6406,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 268, - "target_local_id": 232, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 157, @@ -6830,14 +6694,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 187, - "target_local_id": 262, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 222, @@ -6910,14 +6766,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 263, - "target_local_id": 153, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 254, @@ -7006,14 +6854,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 211, - "target_local_id": 270, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 116, @@ -7086,14 +6926,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 273, - "target_local_id": 24, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 176, @@ -7174,14 +7006,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 219, - "target_local_id": 265, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 262, @@ -7318,14 +7142,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 46, - "target_local_id": 263, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 71, @@ -7366,14 +7182,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 114, - "target_local_id": 271, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 126, @@ -7462,22 +7270,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 238, - "target_local_id": 270, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 41, - "target_local_id": 276, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 92, @@ -7534,14 +7326,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 274, - "target_local_id": 40, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 205, @@ -7598,14 +7382,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 54, - "target_local_id": 266, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 120, @@ -7678,14 +7454,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 128, - "target_local_id": 273, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 190, @@ -7702,22 +7470,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 266, - "target_local_id": 111, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 272, - "target_local_id": 12, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 205, @@ -7782,14 +7534,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 202, - "target_local_id": 264, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 269, @@ -7814,14 +7558,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 233, - "target_local_id": 267, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 196, @@ -7838,14 +7574,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 116, - "target_local_id": 270, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 146, diff --git a/.fixtures/seeds/bilal-port/macro-view.json b/.fixtures/seeds/bilal-port/macro-view.json index 5a9df891f..8020c88d0 100644 --- a/.fixtures/seeds/bilal-port/macro-view.json +++ b/.fixtures/seeds/bilal-port/macro-view.json @@ -3177,22 +3177,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 226, - "target_local_id": 210, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 229, - "target_local_id": 46, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 76, @@ -3337,14 +3321,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 97, - "target_local_id": 229, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 111, @@ -3401,14 +3377,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 56, - "target_local_id": 218, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 88, @@ -4089,14 +4057,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 217, - "target_local_id": 161, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 17, @@ -4225,14 +4185,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 9, - "target_local_id": 228, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 225, @@ -4329,14 +4281,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 222, - "target_local_id": 220, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 164, @@ -4401,14 +4345,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 111, - "target_local_id": 223, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 93, @@ -4449,22 +4385,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 228, - "target_local_id": 38, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 224, - "target_local_id": 99, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 223, @@ -4473,14 +4393,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 218, - "target_local_id": 207, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 177, @@ -4521,14 +4433,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 228, - "target_local_id": 118, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 153, @@ -4681,14 +4585,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 227, - "target_local_id": 209, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 221, @@ -4777,14 +4673,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 229, - "target_local_id": 46, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 219, @@ -4825,14 +4713,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 219, - "target_local_id": 20, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 51, @@ -4865,14 +4745,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 8, - "target_local_id": 217, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 111, @@ -4897,14 +4769,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 190, - "target_local_id": 217, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 220, @@ -4913,14 +4777,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 226, - "target_local_id": 189, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 169, @@ -4985,14 +4841,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 202, - "target_local_id": 221, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 226, @@ -5017,14 +4865,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 221, - "target_local_id": 189, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 219, @@ -5137,14 +4977,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 177, - "target_local_id": 229, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 187, @@ -5153,22 +4985,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 65, - "target_local_id": 226, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 220, - "target_local_id": 118, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 206, @@ -5201,14 +5017,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 223, - "target_local_id": 214, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 220, @@ -5233,14 +5041,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 221, - "target_local_id": 20, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 191, @@ -5273,14 +5073,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 137, - "target_local_id": 218, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 188, @@ -5497,14 +5289,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 225, - "target_local_id": 99, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 156, @@ -5593,14 +5377,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 167, - "target_local_id": 220, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 228, @@ -5649,14 +5425,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 223, - "target_local_id": 178, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 117, @@ -5665,14 +5433,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 179, - "target_local_id": 227, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 45, @@ -5713,14 +5473,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 169, - "target_local_id": 225, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 224, @@ -5801,14 +5553,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 100, - "target_local_id": 220, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 112, @@ -5849,14 +5593,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 220, - "target_local_id": 64, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 219, @@ -6073,14 +5809,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 59, - "target_local_id": 219, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 125, @@ -6089,14 +5817,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 131, - "target_local_id": 224, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 208, @@ -6121,14 +5841,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 217, - "target_local_id": 61, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 2, @@ -6169,14 +5881,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 219, - "target_local_id": 105, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 31, @@ -6257,14 +5961,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 5, - "target_local_id": 227, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 123, @@ -6321,22 +6017,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 217, - "target_local_id": 180, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 80, - "target_local_id": 224, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 155, @@ -6417,22 +6097,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 26, - "target_local_id": 228, - "stance": null, - "basis": "explicit", - "rationale": null - }, - { - "category": "dependency", - "source_local_id": 221, - "target_local_id": 19, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "support", "source_local_id": 90, @@ -6457,14 +6121,6 @@ "basis": "explicit", "rationale": null }, - { - "category": "dependency", - "source_local_id": 221, - "target_local_id": 12, - "stance": null, - "basis": "explicit", - "rationale": null - }, { "category": "dependency", "source_local_id": 108, diff --git a/.pi/extensions/worktree/index.ts b/.pi/extensions/worktree/index.ts new file mode 100644 index 000000000..b385ddf1b --- /dev/null +++ b/.pi/extensions/worktree/index.ts @@ -0,0 +1,541 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import type { Stats } from 'node:fs'; +import { readFile, stat, writeFile } from 'node:fs/promises'; +import { basename, dirname, isAbsolute, join, resolve } from 'node:path'; +import { promisify } from 'node:util'; + +import { + SessionManager, + type ExtensionAPI, + type ExtensionCommandContext, +} from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; + +const execFile = promisify(execFileCallback); + +export const WORKTREE_SWITCH_COMMAND = 'worktree:switch'; +export const WORKTREE_SWITCH_TOOL = 'switch_worktree'; +export const WORKTREE_CREATE_COMMAND = 'worktree:create'; +export const WORKTREE_CREATE_TOOL = 'create_worktree'; + +const DIRTY_WORKTREE_WARNING = + 'Caller worktree has uncommitted changes; the new worktree was created from committed HEAD only.'; + +const DEFAULT_GREEK_WORDS = [ + 'alpha', + 'beta', + 'gamma', + 'delta', + 'epsilon', + 'zeta', + 'eta', + 'theta', + 'iota', + 'kappa', + 'lambda', + 'mu', + 'nu', + 'xi', + 'omicron', + 'pi', + 'rho', + 'sigma', + 'tau', + 'upsilon', + 'phi', + 'chi', + 'psi', + 'omega', +] as const; + +export type WorktreeValidationResult = + | { readonly ok: true; readonly cwd: string } + | { + readonly ok: false; + readonly path: string; + readonly reason: 'missing' | 'not-directory' | 'bare-repository' | 'not-git-worktree'; + }; + +export interface SwitchWorktreeResultDetails { + readonly status: 'staged' | 'switched' | 'cancelled' | 'failed'; + readonly targetPath: string; + readonly sessionFile?: string; + readonly reason?: string; +} + +export interface SwitchWorktreeOptions { + readonly sessionDir?: string; +} + +export interface SiblingWorktreePlan { + readonly path: string; + readonly branch: string; + readonly attempted: readonly string[]; +} + +export interface SiblingWorktreePlanOptions { + readonly sourceRoot: string; + readonly greekWords?: readonly string[]; + readonly chooseStartIndex?: (candidateCount: number) => number; + readonly pathExists: (path: string) => Promise; + readonly branchExists: (branch: string) => Promise; +} + +export interface CreateSiblingWorktreeOptions { + readonly greekWords?: readonly string[]; + readonly chooseStartIndex?: (candidateCount: number) => number; +} + +export type CreateSiblingWorktreeResultDetails = + | { + readonly status: 'created'; + readonly sourceRoot: string; + readonly sourceCommit: string; + readonly path: string; + readonly branch: string; + readonly dirty: boolean; + readonly dirtyWarning?: string; + readonly stdout: string; + readonly stderr: string; + } + | { + readonly status: 'failed'; + readonly reason: string; + readonly sourceRoot?: string; + readonly sourceCommit?: string; + readonly path?: string; + readonly branch?: string; + readonly attempted?: readonly string[]; + readonly stdout?: string; + readonly stderr?: string; + }; + +interface GitProbeResult { + readonly ok: boolean; + readonly stdout: string; + readonly stderr: string; +} + +interface ReplacementMessageContext { + readonly sendUserMessage: (message: string) => Promise; + readonly ui: { + readonly notify: (message: string, type?: 'info' | 'warning' | 'error') => void; + }; +} + +interface WorktreeCreationContext { + readonly cwd: string; + readonly hasUI?: boolean; + readonly ui: { + readonly notify: (message: string, type?: 'info' | 'warning' | 'error') => void; + readonly setEditorText?: (text: string) => void; + }; +} + +export function resolveSwitchTarget(targetPath: string, cwd: string): string { + const trimmed = targetPath.trim(); + if (trimmed.length === 0) return ''; + return isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd, trimmed); +} + +export async function validateGitWorktree(targetPath: string): Promise { + let targetStat: Stats; + try { + targetStat = await stat(targetPath); + } catch { + return { ok: false, reason: 'missing', path: targetPath }; + } + + if (!targetStat.isDirectory()) { + return { ok: false, reason: 'not-directory', path: targetPath }; + } + + const bareProbe = await gitProbe(targetPath, 'rev-parse', '--is-bare-repository'); + if (bareProbe.ok && bareProbe.stdout.trim() === 'true') { + return { ok: false, reason: 'bare-repository', path: targetPath }; + } + + const worktreeProbe = await gitProbe(targetPath, 'rev-parse', '--is-inside-work-tree'); + if (!worktreeProbe.ok || worktreeProbe.stdout.trim() !== 'true') { + return { ok: false, reason: 'not-git-worktree', path: targetPath }; + } + + return { ok: true, cwd: targetPath }; +} + +export async function planSiblingWorktree(options: SiblingWorktreePlanOptions): Promise { + const words = options.greekWords ?? DEFAULT_GREEK_WORDS; + if (words.length === 0) throw new Error('No Greek suffix words configured.'); + + const startIndex = normalizeStartIndex( + options.chooseStartIndex?.(words.length) ?? randomStartIndex(words.length), + words.length, + ); + const parentDir = dirname(options.sourceRoot); + const sourceBasename = basename(options.sourceRoot); + const attempted: string[] = []; + + for (let offset = 0; offset < words.length; offset += 1) { + const word = words[(startIndex + offset) % words.length]; + if (!word) continue; + const name = `${sourceBasename}-${word}`; + attempted.push(name); + + const path = join(parentDir, name); + if (await options.pathExists(path)) continue; + if (await options.branchExists(name)) continue; + + return { path, branch: name, attempted }; + } + + throw new Error(`No available sibling worktree name. Attempted: ${attempted.join(', ')}`); +} + +export async function createSiblingWorktree( + ctx: WorktreeCreationContext, + options: CreateSiblingWorktreeOptions = {}, +): Promise { + const rootProbe = await gitProbe(ctx.cwd, 'rev-parse', '--show-toplevel'); + if (!rootProbe.ok) { + const reason = gitFailureReason('Could not resolve caller git worktree root.', rootProbe); + ctx.ui.notify(reason, 'error'); + return { status: 'failed', reason, stdout: rootProbe.stdout, stderr: rootProbe.stderr }; + } + const sourceRoot = rootProbe.stdout.trim(); + + const headProbe = await gitProbe(ctx.cwd, 'rev-parse', 'HEAD'); + if (!headProbe.ok) { + const reason = gitFailureReason('Could not resolve caller HEAD.', headProbe); + ctx.ui.notify(reason, 'error'); + return { status: 'failed', reason, sourceRoot, stdout: headProbe.stdout, stderr: headProbe.stderr }; + } + const sourceCommit = headProbe.stdout.trim(); + + const dirtyProbe = await gitProbe(ctx.cwd, 'status', '--porcelain'); + if (!dirtyProbe.ok) { + const reason = gitFailureReason('Could not inspect caller worktree status.', dirtyProbe); + ctx.ui.notify(reason, 'error'); + return { + status: 'failed', + reason, + sourceRoot, + sourceCommit, + stdout: dirtyProbe.stdout, + stderr: dirtyProbe.stderr, + }; + } + const dirty = dirtyProbe.stdout.trim().length > 0; + + let plan: SiblingWorktreePlan; + try { + plan = await planSiblingWorktree({ + sourceRoot, + ...(options.greekWords === undefined ? {} : { greekWords: options.greekWords }), + ...(options.chooseStartIndex === undefined ? {} : { chooseStartIndex: options.chooseStartIndex }), + pathExists, + branchExists: (branch) => branchExists(sourceRoot, branch), + }); + } catch (error) { + const reason = error instanceof Error ? error.message : 'Could not plan sibling worktree.'; + ctx.ui.notify(reason, 'error'); + return { status: 'failed', reason, sourceRoot, sourceCommit }; + } + + const addProbe = await gitProbe(sourceRoot, 'worktree', 'add', '-b', plan.branch, plan.path, sourceCommit); + if (!addProbe.ok) { + const reason = gitFailureReason('Could not create sibling git worktree.', addProbe); + ctx.ui.notify(reason, 'error'); + return { + status: 'failed', + reason, + sourceRoot, + sourceCommit, + path: plan.path, + branch: plan.branch, + attempted: plan.attempted, + stdout: addProbe.stdout, + stderr: addProbe.stderr, + }; + } + + const validation = await validateGitWorktree(plan.path); + if (!validation.ok) { + const reason = validationReason(validation); + ctx.ui.notify(reason, 'error'); + return { + status: 'failed', + reason, + sourceRoot, + sourceCommit, + path: plan.path, + branch: plan.branch, + attempted: plan.attempted, + }; + } + + const switchCommand = `/${WORKTREE_SWITCH_COMMAND} ${validation.cwd}`; + if (ctx.hasUI) ctx.ui.setEditorText?.(switchCommand); + if (dirty) ctx.ui.notify(DIRTY_WORKTREE_WARNING, 'warning'); + ctx.ui.notify(`Created git worktree ${validation.cwd} at ${sourceCommit}.`, 'info'); + + const created = { + status: 'created' as const, + sourceRoot, + sourceCommit, + path: validation.cwd, + branch: plan.branch, + dirty, + stdout: addProbe.stdout, + stderr: addProbe.stderr, + }; + return dirty ? { ...created, dirtyWarning: DIRTY_WORKTREE_WARNING } : created; +} + +export async function createRelocatedSession( + sourceSessionFile: string, + targetCwd: string, + sessionDir?: string, +): Promise { + const manager = SessionManager.forkFrom(sourceSessionFile, targetCwd, sessionDir); + const sessionFile = manager.getSessionFile(); + if (!sessionFile) throw new Error('Pi did not persist the relocated session file.'); + await cleanForkedSessionHeader(sessionFile); + return sessionFile; +} + +export async function cleanForkedSessionHeader(sessionFile: string): Promise { + const content = await readFile(sessionFile, 'utf8'); + const lineEnd = content.indexOf('\n'); + const firstLine = lineEnd === -1 ? content : content.slice(0, lineEnd); + if (firstLine.trim().length === 0) return; + + const header = JSON.parse(firstLine) as Record; + if (header.type !== 'session' || !Object.hasOwn(header, 'parentSession')) return; + + delete header.parentSession; + const rest = lineEnd === -1 ? '' : content.slice(lineEnd); + await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`); +} + +export async function runSwitchWorktree( + targetPath: string, + ctx: ExtensionCommandContext, + options: SwitchWorktreeOptions = {}, +): Promise { + const resolvedTarget = resolveSwitchTarget(targetPath, ctx.cwd); + if (resolvedTarget.length === 0) { + ctx.ui.notify('Usage: /worktree:switch ', 'error'); + return { status: 'failed', targetPath: resolvedTarget, reason: 'missing target path' }; + } + + const validation = await validateGitWorktree(resolvedTarget); + if (!validation.ok) { + const reason = validationReason(validation); + ctx.ui.notify(reason, 'error'); + return { status: 'failed', targetPath: resolvedTarget, reason }; + } + + if (ctx.hasUI) { + const confirmed = await ctx.ui.confirm( + 'Switch Pi worktree?', + `Relocate this Pi session to ${validation.cwd}?\n\nThe current session file will be preserved.`, + ); + if (!confirmed) { + ctx.ui.notify('Worktree switch cancelled.', 'info'); + return { status: 'cancelled', targetPath: validation.cwd }; + } + } + + const sourceSessionFile = ctx.sessionManager.getSessionFile(); + if (!sourceSessionFile) { + const reason = 'Current Pi session is not persisted, so it cannot be relocated.'; + ctx.ui.notify(reason, 'error'); + return { status: 'failed', targetPath: validation.cwd, reason }; + } + + const relocatedSessionFile = await createRelocatedSession( + sourceSessionFile, + validation.cwd, + options.sessionDir, + ); + const continuation = continuationPrompt(validation.cwd); + const result = await ctx.switchSession(relocatedSessionFile, { + withSession: async (replacementCtx: ReplacementMessageContext) => { + await replacementCtx.sendUserMessage(continuation); + replacementCtx.ui.notify(`Relocated Pi session to ${validation.cwd}`, 'info'); + }, + }); + + if (result.cancelled) { + return { + status: 'cancelled', + targetPath: validation.cwd, + sessionFile: relocatedSessionFile, + reason: 'session switch cancelled by a Pi hook', + }; + } + + return { status: 'switched', targetPath: validation.cwd, sessionFile: relocatedSessionFile }; +} + +export default function registerWorktreeExtension(pi: ExtensionAPI): void { + pi.registerCommand(WORKTREE_SWITCH_COMMAND, { + description: 'Relocate this Pi session to another git worktree', + handler: async (args, ctx) => { + await runSwitchWorktree(args, ctx); + }, + }); + + pi.registerCommand(WORKTREE_CREATE_COMMAND, { + description: 'Create a sibling git worktree from this cwd HEAD and stage a worktree switch', + handler: async (_args, ctx) => { + await createSiblingWorktree(ctx); + }, + }); + + pi.registerTool({ + name: WORKTREE_SWITCH_TOOL, + label: 'Switch worktree', + description: + 'Validate a target git worktree and stage /worktree:switch in the editor so the user can explicitly relocate this Pi session.', + promptSnippet: + 'switch_worktree validates a target git worktree and stages a /worktree:switch command for user-confirmed Pi session relocation.', + promptGuidelines: [ + 'Call switch_worktree only after the user explicitly asks to move this Pi session to another git worktree.', + 'Do not use switch_worktree to create, delete, prune, or clean up worktrees.', + 'After switch_worktree stages /worktree:switch , tell the user to press Enter if they want to relocate the session.', + ], + parameters: Type.Object({ + path: Type.String({ description: 'Absolute or relative path to the target git worktree.' }), + }), + async execute(_toolCallId, params, _signal, _onUpdate, ctx) { + const resolvedTarget = resolveSwitchTarget(params.path, ctx.cwd); + const validation = await validateGitWorktree(resolvedTarget); + if (!validation.ok) { + const reason = validationReason(validation); + return { + content: [{ type: 'text' as const, text: reason }], + details: { + status: 'failed', + targetPath: resolvedTarget, + reason, + } satisfies SwitchWorktreeResultDetails, + }; + } + + const command = `/${WORKTREE_SWITCH_COMMAND} ${validation.cwd}`; + if (ctx.hasUI) ctx.ui.setEditorText(command); + return { + content: [ + { + type: 'text' as const, + text: ctx.hasUI + ? `Staged ${command}. Press Enter to relocate this Pi session.` + : `Validated ${validation.cwd}. Run ${command} in interactive Pi to relocate this session.`, + }, + ], + details: { status: 'staged', targetPath: validation.cwd } satisfies SwitchWorktreeResultDetails, + }; + }, + }); + + pi.registerTool({ + name: WORKTREE_CREATE_TOOL, + label: 'Create sibling worktree', + description: + 'Create a sibling git worktree from the caller cwd HEAD, then stage /worktree:switch for explicit relocation.', + promptSnippet: + 'create_worktree creates a sibling git worktree from the current cwd committed HEAD and stages /worktree:switch ; it never deletes or prunes worktrees.', + promptGuidelines: [ + 'Call create_worktree only when the user explicitly asks to create a sibling git worktree.', + 'The created worktree is based on the caller cwd HEAD; warn that uncommitted changes are excluded when the caller worktree is dirty.', + 'Do not delete, prune, clean up, or manage existing worktrees after creation.', + 'After create_worktree stages /worktree:switch , tell the user to press Enter if they want to relocate the Pi session.', + ], + parameters: Type.Object({}), + async execute(_toolCallId, _params, _signal, _onUpdate, ctx) { + const details = await createSiblingWorktree(ctx); + if (details.status === 'failed') { + return { + content: [{ type: 'text' as const, text: details.reason }], + details, + }; + } + + const warning = details.dirtyWarning ? `\n\nWarning: ${details.dirtyWarning}` : ''; + return { + content: [ + { + type: 'text' as const, + text: `Created ${details.path} on branch ${details.branch} from ${details.sourceCommit}. Staged /worktree:switch ${details.path}.${warning}`, + }, + ], + details, + }; + }, + }); +} + +function continuationPrompt(targetCwd: string): string { + return `Continue in the relocated Pi session from cwd: ${targetCwd}`; +} + +function validationReason(validation: Extract): string { + switch (validation.reason) { + case 'missing': + return `Target path does not exist: ${validation.path}`; + case 'not-directory': + return `Target path is not a directory: ${validation.path}`; + case 'bare-repository': + return `Target path is a bare git repository, not a working tree: ${validation.path}`; + case 'not-git-worktree': + return `Target path is not a git working tree: ${validation.path}`; + } +} + +function normalizeStartIndex(candidate: number, candidateCount: number): number { + if (!Number.isFinite(candidate) || candidateCount <= 0) return 0; + const whole = Math.trunc(candidate); + return ((whole % candidateCount) + candidateCount) % candidateCount; +} + +function randomStartIndex(candidateCount: number): number { + return Math.floor(Math.random() * candidateCount); +} + +async function pathExists(path: string): Promise { + try { + await stat(path); + return true; + } catch (error) { + return !isNodeError(error) || error.code !== 'ENOENT'; + } +} + +async function branchExists(cwd: string, branch: string): Promise { + const result = await gitProbe(cwd, 'show-ref', '--verify', '--quiet', `refs/heads/${branch}`); + return result.ok; +} + +function gitFailureReason(prefix: string, result: GitProbeResult): string { + const output = [result.stderr.trim(), result.stdout.trim()].filter((part) => part.length > 0).join('\n'); + return output.length > 0 ? `${prefix}\n${output}` : prefix; +} + +async function gitProbe(cwd: string, ...args: string[]): Promise { + try { + const { stdout, stderr } = await execFile('git', args, { cwd }); + return { ok: true, stdout, stderr }; + } catch (error) { + const result = error as { readonly stdout?: unknown; readonly stderr?: unknown }; + return { + ok: false, + stdout: typeof result.stdout === 'string' ? result.stdout : '', + stderr: typeof result.stderr === 'string' ? result.stderr : '', + }; + } +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error; +} diff --git a/docs/README.md b/docs/README.md index ddbccd274..298644764 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,10 @@ planning state: [`.fixtures/`](../.fixtures/) holds current probe-run artifacts and transcript evidence. See the directory README for layout and conventions. +## Testing guides + +- [`docs/testing/seeded-dev-rpc.md`](./testing/seeded-dev-rpc.md) — set up a seeded local Brunch workspace, inspect it over JSON-RPC, use the gated `dev.graph.commitGraph` harness, and run the product-path fixture curation tracer. + ## Behavioral kernels [`docs/design/BEHAVIORAL_KERNELS.md`](../design/BEHAVIORAL_KERNELS.md) is diff --git a/docs/archive/PLAN_HISTORY.md b/docs/archive/PLAN_HISTORY.md index 8d6bc2007..8f4ad27fa 100644 --- a/docs/archive/PLAN_HISTORY.md +++ b/docs/archive/PLAN_HISTORY.md @@ -3,6 +3,13 @@ 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-06-05 Rolling completion archive + +Archived from `memory/PLAN.md` when FE-807 closed and the live frontier advanced to `project-graph-review-cycle`. + +- 2026-06-04 `agents-composition-layer` (FE-806) — Done: `agents/state.ts`/`compose.ts` emit runtime headers and gated prompt-resource manifests; `agents/contexts/{cwd,graph,node}.ts` renders selected-spec context with lens-specific emphasis; the real `.pi` `before_agent_start` product path supplies selected-spec-bound graph snapshots from the Brunch runtime factory; the legacy `src/.pi/context/` prompt-pack subtree is deleted after folding its useful guidance into `src/agents/methods/*.md`; deterministic product-path proof records strategy/lens posture differences and accepted blind spots. Verified: context/compose/prompting/architecture tests and `npm run verify`. Watch: prompt quality is fitness evidence only; graph-write resilience and capture quality remain with the next P0 frontiers. +- 2026-06-04 `live-graph-observer` (FE-795) — Done: `graph.overview` and `graph.nodeNeighborhood` are discoverable selected-spec RPC reads; graph readers remain in `graph/`; TUI/agent `commit_graph` publishes graph invalidation topics through the shared product-update bus; the TUI launch path starts a read-only web sidecar over the same bus; the React web app attaches over one WebSocket RPC client, renders the selected-spec graph overview, and invalidates/refetches canonical graph readers on `brunch.updated`. Verified: targeted FE-795 test set (`src/rpc/handlers.test.ts`, `src/rpc/web-host.test.ts`, `src/web/app.test.tsx`, `src/brunch-tui.test.ts`, `src/graph/snapshot.test.ts`, `src/graph/spec-ownership.test.ts`), `npm run build`, and a 2026-06-04 `agent-browser` smoke that observed empty graph state then a `commit_graph`-created node in the browser without reload. Watch: richer node-neighborhood UI remains optional polish; the current proof exposes/query-backs the focused read and renders the overview. + ## 2026-06-04 FE-808 closeout archive Archived from `memory/PLAN.md` when FE-808 closed out and the live frontier advanced to `capture-response-to-graph`. diff --git a/docs/testing/seeded-dev-rpc.md b/docs/testing/seeded-dev-rpc.md new file mode 100644 index 000000000..fa1a0b760 --- /dev/null +++ b/docs/testing/seeded-dev-rpc.md @@ -0,0 +1,171 @@ +# Seeded local dev RPC workflow + +Use this guide when you want a practical local Brunch workspace populated with reusable seed fixtures, while still being able to inspect and mutate that workspace from an agent conversation through JSON-RPC. + +This is a **local development harness**: + +- reusable seed files under `.fixtures/seeds/**` are explicit starting truth; +- `dev.graph.commitGraph` is opt-in and routes through `CommandExecutor`, but is not a product API; +- product-flow proof still comes from transcript-backed runs that use the real agent tools (`read_graph` / `commit_graph`). + +## 0. Choose an isolated workspace + +Prefer a workbench directory so seeded `.brunch/` state does not mix with whatever is in the repo root. + +```bash +REPO="$(git rev-parse --show-toplevel)" +WORKSPACE="$REPO/.fixtures/workbenches/seeded-dev-rpc" +mkdir -p "$WORKSPACE" +``` + +To reset this scratch workspace only: + +```bash +rm -rf "$WORKSPACE/.brunch" +``` + +Do not run that cleanup command against a workspace whose Brunch sessions or graph data you care about. + +## 1. Seed all current fixtures + +Run the seed loader from the target workspace. It loads every `.fixtures/seeds//.json` through `CommandExecutor` into `$WORKSPACE/.brunch/data.db`. + +```bash +( + cd "$WORKSPACE" + "$REPO/node_modules/.bin/tsx" "$REPO/src/graph/seed-fixtures.ts" +) +``` + +Current seed sets include: + +- `bilal-port/*` — full Bilal-derived specs. +- `bilal-port-variants/macro-view-grounded-intent` — explicit-basis grounded-intent base variant for curation/proposal tests. + +The loader currently seeds all sets. Inspect the actual spec ids before issuing graph calls; do not assume a fixed id ordering. + +## 2. Define a one-shot dev RPC helper + +`--mode=rpc` is a JSON-RPC line server over stdio. For command-line work, it is easiest to send one or more JSON lines and let the process exit at EOF. + +```bash +brunch_rpc() { + local payload="$1" + ( + cd "$WORKSPACE" + printf '%s\n' "$payload" | \ + BRUNCH_DEV_RPC=1 "$REPO/node_modules/.bin/tsx" "$REPO/src/brunch.ts" --mode=rpc + ) +} +``` + +`BRUNCH_DEV_RPC=1` enables `dev.graph.commitGraph`. Without that env var, the method is absent from discovery and calls return `Method not found`. + +RPC output may include `brunch.updated` notifications as separate JSON lines. Filter responses by `id` when scripting: + +```bash +brunch_rpc '{"jsonrpc":"2.0","id":1,"method":"rpc.discover"}' \ + | jq 'select(.id == 1).result.methods[].method' +``` + +## 3. Inspect seeded specs + +```bash +brunch_rpc '{"jsonrpc":"2.0","id":2,"method":"workspace.selectionState"}' \ + | jq 'select(.id == 2).result.specs[] | {id: .spec.id, title: .spec.title, sessions: (.sessions | length)}' +``` + +Pick the `specId` you want to inspect or mutate: + +```bash +SPEC_ID=1 +``` + +Read the graph overview: + +```bash +brunch_rpc "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"graph.overview\",\"params\":{\"specId\":$SPEC_ID}}" \ + | jq 'select(.id == 3).result | {nodeCount, edgeCount, lsn, goals: [.nodes[] | select(.kind == "goal") | {id, code: ("G" + (.kindOrdinal|tostring)), title}]}' +``` + +Projected node codes are not stored in the DB. They are rendered from `kind` + `kindOrdinal` using the graph labels (`G1`, `TH1`, `T1`, `CTX1`, `R1`, `CR1`, etc.). Use `graph.overview` to find the current `kindOrdinal` before referencing existing nodes by code. + +## 4. Activate a session when session methods matter + +Graph reads and `dev.graph.commitGraph` take explicit `specId` and do not require a selected session. Session methods do. + +Create a new session for a seeded spec: + +```bash +brunch_rpc "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"workspace.activate\",\"params\":{\"decision\":{\"action\":\"newSession\",\"specId\":$SPEC_ID}}}" \ + | jq 'select(.id == 4).result | {status, spec, session}' +``` + +Then session calls such as `session.triggerExchange`, `session.pendingExchange`, `session.submitExchangeResponse`, and `session.runtimeState` operate on that selected session unless you pass an explicit session target where supported. + +## 5. Make a dev graph commit + +Use `dev.graph.commitGraph` for exact local curation or seam testing. Default to `basis: "explicit"` when you are manually authoring fixture truth. + +The example below adds a thesis and connects it to an existing goal. Replace `G1` with a real code from your `graph.overview` output. + +```bash +cat > /tmp/brunch-dev-commit.json </ +├── session.jsonl +├── transcript.md +├── report.json +└── graph-snapshot.json +``` + +The existing reference run is `.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/`. Its report shows 70 explicit base nodes plus implicit product-created nodes/edges from one real `commit_graph` tool call. + +## 7. Browser/TUI notes + +The TUI-started web sidecar is read-only. It can observe graph updates from the same host, but it does not expose `dev.graph.commitGraph`. + +For agent-addressable dev mutations, run a separate `BRUNCH_DEV_RPC=1 --mode=rpc` command against the same workspace directory. Keep to the one-writer discipline: do not run concurrent dev RPC writes and TUI/agent writes against the same workspace unless you are deliberately testing concurrency behavior. + +## Troubleshooting + +- `Method not found` for `dev.graph.commitGraph`: check `BRUNCH_DEV_RPC=1` and ensure you are using `--mode=rpc`, not the TUI-started web sidecar. +- `graph node code "G1" does not resolve`: inspect `graph.overview` for the selected `specId`; codes are spec-scoped. +- Empty `workspace.selectionState`: check that you seeded from the same `$WORKSPACE` directory you are using for RPC. +- Stale or surprising graph state: reset only the scratch workspace with `rm -rf "$WORKSPACE/.brunch"`, then reseed. diff --git a/drizzle/0001_aspiring_orphan.sql b/drizzle/0001_aspiring_orphan.sql index 0d2b70878..2ad8198dd 100644 --- a/drizzle/0001_aspiring_orphan.sql +++ b/drizzle/0001_aspiring_orphan.sql @@ -8,5 +8,20 @@ CREATE TABLE `node_kind_counters` ( ); --> statement-breakpoint CREATE UNIQUE INDEX `node_kind_counters_spec_plane_kind_unique` ON `node_kind_counters` (`spec_id`,`plane`,`kind`);--> statement-breakpoint -ALTER TABLE `nodes` ADD `kind_ordinal` integer NOT NULL;--> statement-breakpoint +UPDATE `nodes` SET `basis` = 'explicit' WHERE `basis` = 'accepted_review_set';--> statement-breakpoint +UPDATE `edges` SET `basis` = 'explicit' WHERE `basis` = 'accepted_review_set';--> statement-breakpoint +ALTER TABLE `nodes` ADD `kind_ordinal` integer;--> statement-breakpoint +UPDATE `nodes` +SET `kind_ordinal` = ( + SELECT count(*) + FROM `nodes` `prior_nodes` + WHERE `prior_nodes`.`spec_id` = `nodes`.`spec_id` + AND `prior_nodes`.`plane` = `nodes`.`plane` + AND `prior_nodes`.`kind` = `nodes`.`kind` + AND `prior_nodes`.`id` <= `nodes`.`id` +);--> statement-breakpoint +INSERT INTO `node_kind_counters` (`spec_id`, `plane`, `kind`, `next_ordinal`) +SELECT `spec_id`, `plane`, `kind`, max(`kind_ordinal`) + 1 +FROM `nodes` +GROUP BY `spec_id`, `plane`, `kind`;--> statement-breakpoint CREATE UNIQUE INDEX `nodes_spec_plane_kind_ordinal_unique` ON `nodes` (`spec_id`,`plane`,`kind`,`kind_ordinal`); \ No newline at end of file diff --git a/memory/PLAN.md b/memory/PLAN.md index 8d5910930..dd7510333 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -31,14 +31,12 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple ### Active -_None._ +1. `project-graph-review-cycle` — P1 unless demo narrative promotes it: real `project-graph` review-set proposal/approval loop. ### Next -1. `capture-response-to-graph` — P0 product loop: structured exchange answer → narrow high-confidence capture → `CommandExecutor` commit → web graph update. -2. `project-graph-review-cycle` — P1 unless demo narrative promotes it: real `project-graph` review-set proposal/approval loop. -3. `minimal-authority-shell` — P1 safety: thin POC authority posture over already-existing command-result seams and `elicit` tool policy. -4. `poc-live-ship-gate` — P1 final gate: fresh-cwd runbook exercising the composed product path end to end. +1. `minimal-authority-shell` — P1 safety: thin POC authority posture over already-existing command-result seams and `elicit` tool policy. +2. `poc-live-ship-gate` — P1 final gate: fresh-cwd runbook exercising the composed product path end to end. ### Parallel / Low-conflict @@ -85,9 +83,9 @@ _None._ - **Name:** Structured response capture into selected-spec graph truth - **Linear:** [FE-807](https://linear.app/hash/issue/FE-807/structured-response-capture-into-selected-spec-graph-truth) -- **Branch:** to create — `ln/fe-807-capture-response-to-graph` +- **Branch:** `ln/fe-807-capture-response-to-graph` - **Kind:** structural / tracer bullet -- **Status:** next +- **Status:** done - **Certainty:** proving - **Stabilizes:** I30-L, I31-L, I39-L, I40-L — capture must aim at the selected-spec graph through stable projected node-code/basis semantics rather than raw ids or path-shaped basis values. - **Lights up:** structured exchange response → explicit-basis graph truth → selected-spec web observer update. @@ -104,6 +102,8 @@ _None._ - **Cross-cutting obligations:** Preserve D4-L/D20-L single-authority mutation; keep capture synchronous and bounded for POC; do not introduce deferred observer/auditor queues or canonical chat/turn tables here. Capture must respect D61-L: claims are node-level truth inside the selected spec. Preserve D62-L/D63-L/D64-L: projected codes are presentation handles, basis is approval strength, and readiness bands guide capture objectives without becoming kind whitelists. - **Traceability:** R10, R16, R17, R21, R22 / D4-L, D17-L, D18-L, D20-L, D21-L, D45-L, D52-L, D54-L, D56-L, D57-L, D61-L, D62-L, D63-L, D64-L / I30-L, I31-L, I39-L, I40-L / A22-L, A3-L. - **Design docs:** `docs/design/GRAPH_MODEL.md`; `docs/design/ELICITATION_LENSES.md`; `memory/SPEC.md` D17-L/D18-L/D61-L. +- **Current execution pointer:** Complete. `session.submitExchangeResponse` now appends the terminal structured-exchange response, synchronously captures directly labeled text facts via `graph/capture/structured-response.ts`, commits selected-spec `basis: explicit` graph nodes through `CommandExecutor`, returns `captured | no_capture | structural_illegal`, and publishes graph invalidations. Broader LLM extraction quality, reconciliation-need capture, and readiness-grade capture remain future fitness/work. + ### graph-tool-resilience @@ -139,9 +139,9 @@ _None._ - **Name:** Project-graph review-set proposal and atomic acceptance - **Linear:** [FE-809](https://linear.app/hash/issue/FE-809/project-graph-review-set-proposal-and-atomic-acceptance) -- **Branch:** to create — `ln/fe-809-project-graph-review-cycle` +- **Branch:** `ln/fe-809-project-graph-review-cycle` - **Kind:** structural / bounded feature -- **Status:** next +- **Status:** active - **Certainty:** proving - **Stabilizes:** I34-L, I40-L — exact review approval must become one explicit-basis atomic graph batch, not a path-shaped basis value or partial commit. - **Lights up:** `project-graph` proposal → dry-run-valid `present_review_set` → approval → `acceptReviewSet` graph commit. @@ -158,6 +158,7 @@ _None._ - **Cross-cutting obligations:** Preserve D27-L: review-set proposal is a structured-exchange payload, not a standalone public review-set entity. Reviewer advisory writes remain deferred unless explicitly scoped. Existing-node references and review payloads use projected graph codes at adapter/UI boundaries, not raw DB ids. - **Traceability:** R21, R23 / D4-L, D20-L, D26-L, D27-L, D51-L, D53-L, D62-L, D63-L / I11-L, I34-L, I40-L / A14-L, A16-L. - **Design docs:** `docs/design/REVIEW_SETS.md`; `docs/design/GRAPH_MODEL.md`; `memory/SPEC.md` D27-L. +- **Current execution pointer:** Active; first FE-809 scope card pending. ### minimal-authority-shell @@ -246,30 +247,34 @@ _None._ - **Acceptance:** - Seed contract stays loadable: each set's port script self-validates every `.json` through the real loader (same structural checks `commitGraph` enforces) before writing. - `npm run seed` loads every `.fixtures/seeds//.json` into the workspace DB through `CommandExecutor` (never direct row inserts), preserving graph clock / change log / lsn coherence. - - New seed sets follow the established shape: vendored `_originals/`, throwaway `_port-script.ts`, consolidated `.json`, generated `README.md`. + - New seed sets follow the established shape: vendored `_originals/`, throwaway `_port-script.ts`, consolidated `.json`, generated `README.md`; derived variant sets may instead document the deterministic filter over an existing seed set and keep mixed-basis product-run output under `.fixtures/runs/`. + - Product curation runs over seeds leave transcript-backed artifacts (`session.jsonl`, `transcript.md`, `report.json`, and graph readback when graph truth is the proof target) and prove real `commit_graph` transcript evidence plus implicit graph rows; mixed-basis snapshots are not registered as reusable seeds. - **Enhancement backlog (captured, not yet scoped):** 1. Enhance Bilal-port fixtures *through Brunch itself* by feeding the original briefs Bilal authored, to recover `thesis`/`goal` structure the current ported graphs under-express. 2. Port and enhance the earlier product version's fixtures (the legacy walkthrough scenarios in `docs/praxis/manual-testing.md`), raising quality through better semantic definition (kinds, detail) and internal connection (edges). -- **Verification:** Inner — `src/graph/seed-fixtures.test.ts` seeds a real fixture into an in-memory DB and asserts spec/node/edge counts plus change-log/clock coherence, and rejects non-`explicit` basis; port-script self-validation gates output. Outer — `npm run seed` smoke against a fresh cwd. +- **Verification:** Inner — `src/graph/seed-fixtures.test.ts` seeds a real fixture into an in-memory DB and asserts spec/node/edge counts plus change-log/clock coherence, rejects non-`explicit` basis, and covers the `macro-view-grounded-intent` explicit intent-only variant; `src/probes/fixture-curation-loop.test.ts` proves curation report/artifact evidence detection without an LLM. Outer — `npm run seed` smoke against a fresh cwd; real fixture-curation runs under `.fixtures/runs/fixture-curation/`. - **Topology materialization:** Seed data and throwaway prep scripts live under `.fixtures/seeds/`; the loader lives in `src/graph/seed-fixtures.ts` (graph/ owns `CommandExecutor` orchestration; db/ is imported only by graph/, never the reverse); no seed-only graph runtime the product launch does not use. - **Cross-cutting obligations:** Seeds commit only through `CommandExecutor`; directly-authored items use `basis: explicit` (the retired `accepted_review_set` value is not a basis). Respect multi-spec discipline — each fixture is one spec's own graph (D61-L). Pre-release posture: regenerate fixtures when the schema moves rather than preserving stale shapes. **Known drift:** `docs/praxis/manual-testing.md` still describes the earlier seed system (scenario-arg `npm run seed`, `.brunch/brunch.db`); reconcile it to the current loader (all-sets `npm run seed`, `.brunch/data.db`) when the legacy port (backlog item 2) lands — coordinate with the doc-reconciliation track rather than double-editing. -- **Traceability:** D4-L, D20-L, D52-L, D61-L, D62-L, D63-L / A14-L. +- **Current execution pointer:** No active scope file. Product-driven fixture-curation tracer landed: `macro-view-grounded-intent` is a deterministic explicit-basis Bilal variant, and `.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/` proves one real `propose-graph`/`commit_graph` run created implicit intent nodes from that base. Next `dev-seed-fixtures` scope may review curation fitness and decide whether variants need smaller prompts, richer base profiles, or a reusable mixed-basis export step. +- **Traceability:** D4-L, D19-L, D20-L, D52-L, D61-L, D62-L, D63-L / A14-L. - **Design docs:** `.fixtures/seeds/bilal-port/README.md`; `docs/design/GRAPH_MODEL.md`; `docs/praxis/manual-testing.md`. ## Recently Completed +- 2026-06-05 `capture-response-to-graph` (FE-807) — Done: synchronous response-capture tracer. Added a narrow labeled-text translator for `Goal:`, `Context:`, `Constraint:`, and `Criterion:` facts; wired public `session.submitExchangeResponse` to capture through the transcript binding's spec and `CommandExecutor.commitGraph({basis: explicit})`; returned loud capture outcomes; published graph invalidations; and added a public-RPC proof that activation/trigger/submit/overview exposes captured projected codes. Verified: `src/graph/capture/structured-response.test.ts`, `src/rpc/handlers.test.ts`, `src/probes/capture-response-to-graph-proof.test.ts`. + +- 2026-06-05 `dev-seed-fixtures` — Done: first product-driven fixture curation tracer. Added deterministic `bilal-port-variants/macro-view-grounded-intent` explicit-only intent base, a `fixture-curation` probe runner/report summarizer, and run artifacts proving `gpt-5.5` used real `read_graph`/`commit_graph` product tools to persist two implicit requirement nodes plus six implicit edges through `CommandExecutor`. Verified: `src/probes/fixture-curation-loop.test.ts`, `src/graph/seed-fixtures.test.ts`, real run `.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/`. + - 2026-06-04 `graph-tool-resilience` (FE-808) — Done: graph nodes persist per-kind ordinals and expose projected codes; `commitGraph` applies one explicit/implicit batch basis, returns one created-node identity shape, plans once inside the transaction before LSN allocation/writes, and shares dry-run/commit structural validation; adapters resolve selected-spec existing-node codes into structured diagnostics without sentinel endpoint refs or thrown errors; single-node `createNode` rejects retired basis values before LSN/counter/node/change-log allocation; same-spec supersession cycles are rejected atomically; active-context graph reads omit hidden superseded nodes and dangling edges while graph-truth reads remain available; product-path probes landed existing-code, retry-diagnostics, and ambiguity/no-overcommit evidence under `.fixtures/runs/propose-graph-commit/`. -- 2026-06-04 `agents-composition-layer` (FE-806) — Done: `agents/state.ts`/`compose.ts` emit runtime headers and gated prompt-resource manifests; `agents/contexts/{cwd,graph,node}.ts` renders selected-spec context with lens-specific emphasis; the real `.pi` `before_agent_start` product path supplies selected-spec-bound graph snapshots from the Brunch runtime factory; the legacy `src/.pi/context/` prompt-pack subtree is deleted after folding its useful guidance into `src/agents/methods/*.md`; deterministic product-path proof records strategy/lens posture differences and accepted blind spots. Verified: context/compose/prompting/architecture tests and `npm run verify`. Watch: prompt quality is fitness evidence only; graph-write resilience and capture quality remain with the next P0 frontiers. -- 2026-06-04 `live-graph-observer` (FE-795) — Done: `graph.overview` and `graph.nodeNeighborhood` are discoverable selected-spec RPC reads; graph readers remain in `graph/`; TUI/agent `commit_graph` publishes graph invalidation topics through the shared product-update bus; the TUI launch path starts a read-only web sidecar over the same bus; the React web app attaches over one WebSocket RPC client, renders the selected-spec graph overview, and invalidates/refetches canonical graph readers on `brunch.updated`. Verified: targeted FE-795 test set (`src/rpc/handlers.test.ts`, `src/rpc/web-host.test.ts`, `src/web/app.test.tsx`, `src/brunch-tui.test.ts`, `src/graph/snapshot.test.ts`, `src/graph/spec-ownership.test.ts`), `npm run build`, and a 2026-06-04 `agent-browser` smoke that observed empty graph state then a `commit_graph`-created node in the browser without reload. Watch: richer node-neighborhood UI remains optional polish; the current proof exposes/query-backs the focused read and renders the overview. -Older history (including `agent-graph-integration`, `spec-persistence-and-startup`, `sealed-pi-profile-runtime-state`, `pi-ui-extension-patterns`, `web-shell`, `jsonl-session-viability`, `mode-shell-and-fixture-driver`, `walking-skeleton`): `docs/archive/PLAN_HISTORY.md` +Older history (including `agents-composition-layer`, `live-graph-observer`, `agent-graph-integration`, `spec-persistence-and-startup`, `sealed-pi-profile-runtime-state`, `pi-ui-extension-patterns`, `web-shell`, `jsonl-session-viability`, `mode-shell-and-fixture-driver`, `walking-skeleton`): `docs/archive/PLAN_HISTORY.md` ## Dependencies ```text nodes: graph-tool-resilience [done · P0] materialized graph write contract and broadened A14 proof - capture-response-to-graph [next · P0] structured answer -> graph truth -> observer update - project-graph-review-cycle [next · P1] real project-graph review-set approval loop + capture-response-to-graph [done · P0] structured answer -> graph truth -> observer update + project-graph-review-cycle [active · P1] real project-graph review-set approval loop minimal-authority-shell [next · P1] thin safety posture for current POC paths poc-live-ship-gate [next · P1] final fresh-cwd composed product runbook probes-and-transcripts-evolution [parallel] continuous evidence substrate diff --git a/memory/SPEC.md b/memory/SPEC.md index 54e5e277d..51af90f99 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -109,7 +109,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | 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 session-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 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 | partially validated | D27-L, D51-L, D53-L | **CommitGraph subclaim validated 2026-06-02** by the product-path `propose-graph-commit` probe: the default Brunch runtime factory registered real `read_graph`/`commit_graph` tools, `claude-opus-4-7` produced one structurally legal `commit_graph` batch on the first attempt, and `CommandExecutor` persisted 4 nodes + 4 edges (LSN 2). Artifacts: `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`. **Existing-code product-path subclaim validated 2026-06-04** by `.fixtures/runs/propose-graph-commit/2026-06-04-existing-code-ref/`: the default Brunch runtime factory exposed real `read_graph`/`commit_graph`, `gpt-5.5` read projected code `G1`, used `{existingCode: "G1"}` in `commit_graph`, and persisted a requirement plus edge to the pre-existing selected-spec goal without cross-spec writes. **Retry-diagnostics subclaim validated 2026-06-04** by `.fixtures/runs/propose-graph-commit/2026-06-04-retry-diagnostics/`: the default runtime recorded one `structural_illegal` proof-edge attempt with stance diagnostics, then a corrected retry that persisted two nodes and one legal proof edge with no partial state from the failed attempt. **Ambiguity/no-overcommit subclaim validated 2026-06-04** by `.fixtures/runs/propose-graph-commit/2026-06-04-ambiguity-no-overcommit/`: the default runtime read graph context, asked for more concrete accepted facts, and recorded zero `commit_graph` attempts with zero graph nodes/edges written. **Review-set dry-run substrate validated 2026-06-02** by `review-set-proposal.test.ts`: reviewable proposals must carry lens/epistemic/grounding metadata, translate to `commitGraph` input, pass `CommandExecutor.dryRunCommitGraph`, and stay off the review surface when structurally illegal. Remaining open subclaim: real LLM `project-graph` proposal generation against that substrate. Fallback options remain constrained generation, retry-with-feedback, or NL-parse-at-accept if later legality rates regress. | +| 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 | partially validated | D27-L, D51-L, D53-L | **CommitGraph subclaim validated 2026-06-02** by the product-path `propose-graph-commit` probe: the default Brunch runtime factory registered real `read_graph`/`commit_graph` tools, `claude-opus-4-7` produced one structurally legal `commit_graph` batch on the first attempt, and `CommandExecutor` persisted 4 nodes + 4 edges (LSN 2). Artifacts: `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`. **Existing-code product-path subclaim validated 2026-06-04** by `.fixtures/runs/propose-graph-commit/2026-06-04-existing-code-ref/`: the default Brunch runtime factory exposed real `read_graph`/`commit_graph`, `gpt-5.5` read projected code `G1`, used `{existingCode: "G1"}` in `commit_graph`, and persisted a requirement plus edge to the pre-existing selected-spec goal without cross-spec writes. **Retry-diagnostics subclaim validated 2026-06-04** by `.fixtures/runs/propose-graph-commit/2026-06-04-retry-diagnostics/`: the default runtime recorded one `structural_illegal` proof-edge attempt with stance diagnostics, then a corrected retry that persisted two nodes and one legal proof edge with no partial state from the failed attempt. **Ambiguity/no-overcommit subclaim validated 2026-06-04** by `.fixtures/runs/propose-graph-commit/2026-06-04-ambiguity-no-overcommit/`: the default runtime read graph context, asked for more concrete accepted facts, and recorded zero `commit_graph` attempts with zero graph nodes/edges written. **Seeded-fixture curation subclaim validated 2026-06-05** by `.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/`: the default runtime loaded an explicit Bilal-derived base through `seedFixture`/`CommandExecutor`, `gpt-5.5` used real `read_graph`/`commit_graph`, and graph readback distinguished 70 explicit base nodes from two implicit product-created requirement nodes and six implicit edges. **Review-set dry-run substrate validated 2026-06-02** by `review-set-proposal.test.ts`: reviewable proposals must carry lens/epistemic/grounding metadata, translate to `commitGraph` input, pass `CommandExecutor.dryRunCommitGraph`, and stay off the review surface when structurally illegal. Remaining open subclaim: real LLM `project-graph` proposal generation against that substrate. Fallback options remain constrained generation, retry-with-feedback, or NL-parse-at-accept if later legality rates regress. | | A15-L | Establishment hints carried as structured-exchange payload facets provide sufficient inspectability, fixture-ability, and ambient-affordance source without a separate establishment-needs graph substrate or standalone `brunch.establishment_offer` entry family; whether such a substrate ever shares storage with reconciliation needs can be deferred. | medium | open | D25-L, D30-L | M5+: fixture inspection confirms establishment-offer facets are reconstructable from transcript-backed structured exchanges; chrome/web orientation regions render ambient affordances from the latest such facet. | | 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. | @@ -117,7 +117,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | 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 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-grade 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. | +| A22-L | The elicitor can perform synchronous post-exchange capture well enough for the POC: high-confidence extractive facts and readiness-grade updates can be committed immediately, while low-confidence implications can be kept out of graph truth and used as disambiguation material. | medium | partially validated | D18-L, D26-L, D45-L, I30-L | 2026-06-05 `capture-response-to-graph` validated the product wiring for narrow labeled text facts (`Goal:`, `Context:`, `Constraint:`, `Criterion:`): `session.submitExchangeResponse` commits through `graph/capture` → `CommandExecutor.commitGraph({basis: explicit})`, returns `captured | no_capture | structural_illegal`, targets the transcript binding's spec, and publishes graph invalidations. Broader LLM capture quality and readiness-grade updates remain fitness evidence. | ### Active Decisions @@ -126,6 +126,7 @@ 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 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/.pi/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/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/.pi/extensions/*`, and reusable Pi TUI components live under `src/.pi/components/*`, so they can also be iterated by launching Pi from `src/` 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; extension/component tests live under `src/.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/.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. + - Tooling exception: root `.pi/extensions/worktree/index.ts` is a project-local developer convenience for direct Pi sessions only. It is not a Brunch product extension, is not imported by `src/.pi/pi-extension-shell.ts`, and does not weaken the sealed Brunch Pi Profile; Brunch-launched product sessions continue to disable ambient `.pi/` discovery unless deliberately imported. The extension may register direct-Pi `/worktree:switch` / `switch_worktree` and `/worktree:create` / `create_worktree` affordances: switching preserves the old session file by default, creation uses the caller cwd's committed `HEAD` and a sibling `-` branch/path, dirty worktrees warn that uncommitted changes are excluded, and generated worktrees are never auto-deleted or pruned. - **D40-L — Runtime state is transcript-backed Brunch session-agent state, not hidden extension memory.** `src/session/runtime-state.ts` owns the pure projection. It reconstructs agent posture from linear `brunch.agent_runtime_state` entries (`reason: "init" | "switch"`), last-writer-wins at turn preparation and over `session.runtimeState`; default/empty slots are explicit when no entry family exists. Its axes are `op_mode` (`elicit`, future `execute`) plus optional, AUTO-able objective axes `strategy`, `lens`, and `goal` (D25-L, D59-L). `session.runtimeState` also exposes shaped mention slots, world-update watermarks (latest graph LSN and optional git head, without raw transcript detail bags), and lifecycle facts when transcript-backed entries make them computable; this is a projection contract, not a mutable state table. The **foreground session agent** (`elicitor` now, future `executor`) is *derived* from `op_mode`, not stored; the other agent roles (`reviewer`, `reconciler`, future `scout`/`researcher`) are async sub-agent/side-chain workers (D29-L, D44-L) invoked out-of-band, never part of the session state machine. `op_mode` gates tool authority, applied by `src/.pi/extensions/operational-mode.ts` (current `elicit` policy denies side-effecting `bash`/`edit`/`write` plus user-shell interception) while `.pi` reuses session-owned state definitions. Prompt composition is a separate concern (D58-L). Depends on: D17-L, D23-L, D25-L, D39-L, D58-L, D59-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority, storing the foreground role as independent session state, the "runtime bundle / role preset" as one knob deriving model/thinking/resources, and binding prompt-resource location to `src/.pi/context/`. - **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/.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. @@ -159,7 +160,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D5-L — Brunch JSON-RPC is the single public product protocol.** Brunch exposes one public product RPC surface over stdio, WebSocket, and in-process handlers. Product clients — web UI, CLI probes, TUI adapters, and future relays — call Brunch method families and should not coordinate raw Pi RPC plus Brunch product RPC themselves. Pi RPC may be used behind a Brunch adapter for agent-loop mechanics and Pi extension UI, but it is not a second public product API. HTTP exists only as a transport shim (static bundle, health, uploads, webhooks). The Brunch stdio surface is also the agent-as-user probe driver interface, even when that driver internally relays Pi RPC events. Depends on: A5-L. Supersedes: treating raw Pi RPC as the product API for Brunch data. - **D10-L — Web client is a native Brunch React app over one WebSocket RPC client.** TanStack Router + TanStack Query + Brunch-owned elicitation/transcript primitives (Vercel AI SDK UI or TanStack AI style). `pi-web-ui` is not reused. The browser is a thin remote head over Brunch RPC method families, not a second product runtime or REST-backed data client. Depends on: D5-L. Supersedes: —. - **D17-L — Brunch semantics ride one transcript/event substrate, not parallel channels.** Pi JSONL transcript entries — ordinary messages, assistant tool-call/toolResult exchanges, and custom messages/entries — plus `deliverAs: "nextTurn" | "followUp"` and `prepareNextTurn` are the load-bearing mechanism for structured elicitation prompts/responses, `worldUpdate`, mention-staleness hints, and side-task-result delivery. New product semantics should compose onto this substrate before inventing a second event plane or a parallel chat/turn store. Depends on: D5-L, D6-L, D12-L, D15-L. Supersedes: custom-message-only interpretations of structured elicitation. -- **D19-L — Keep product RPC/read architecture thin: named method families over projection handlers.** Brunch exposes concrete named methods, not vague feature buckets or generic records. The canonical public RPC vocabulary is maintained in [`src/rpc/README.md`](file:///Users/lunelson/Code/hashintel/brunch-next/src/rpc/README.md): `rpc.discover`; `workspace.snapshot`, `workspace.selectionState`, `workspace.activate`; current selected-spec graph reads `graph.overview` and `graph.nodeNeighborhood`; and session methods `session.triggerExchange`, `session.pendingExchange`, `session.submitExchangeResponse`, `session.exchanges`, and `session.runtimeState`. Reserved future target names such as `session.submitMessage`, `graph.changesSince`, and graph-adjacent `graph.coherenceSummary` must not appear in discovery until real behavior exists. Each read handler projects from the canonical store that owns the fact (Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log), and each mutation handler routes to the Brunch-owned command/session layer. `brunch.updated` notifications are first-class JSON-RPC records over WebSocket and stdio, but they are process-local invalidation hints carrying `{topic, specId?, sessionId?, nodeId?, lsn?}`, not canonical truth and not a durable cross-store event spine. Brunch must not create a generic read-gateway platform, REST read model, DB-backed chat/turn projection, or canonical cross-store event spine merely to keep clients in sync. Retired public names are quarantined in `src/rpc/README.md` §Names absent from current public RPC and must not re-enter product discovery. Depends on: D5-L, D6-L, D10-L, D16-L. Supersedes: the heavier “unified read gateway” mental model, vague `elicitation.*` / `command.*` public families, and any discovery/dispatch split where a surface describes methods it rejects. +- **D19-L — Keep product RPC/read architecture thin: named method families over projection handlers.** Brunch exposes concrete named methods, not vague feature buckets or generic records. The canonical public RPC vocabulary is maintained in [`src/rpc/README.md`](file:///Users/lunelson/Code/hashintel/brunch-next/src/rpc/README.md): `rpc.discover`; `workspace.snapshot`, `workspace.selectionState`, `workspace.activate`; current selected-spec graph reads `graph.overview` and `graph.nodeNeighborhood`; and session methods `session.triggerExchange`, `session.pendingExchange`, `session.submitExchangeResponse`, `session.exchanges`, and `session.runtimeState`. Reserved future target names such as `session.submitMessage`, `graph.changesSince`, and graph-adjacent `graph.coherenceSummary` must not appear in discovery until real behavior exists. Each read handler projects from the canonical store that owns the fact (Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log), and each mutation handler routes to the Brunch-owned command/session layer. `brunch.updated` notifications are first-class JSON-RPC records over WebSocket and stdio, but they are process-local invalidation hints carrying `{topic, specId?, sessionId?, nodeId?, lsn?}`, not canonical truth and not a durable cross-store event spine. Brunch must not create a generic read-gateway platform, REST read model, DB-backed chat/turn projection, or canonical cross-store event spine merely to keep clients in sync. Dev-only RPC harness methods may exist under `dev.*` when explicitly enabled for local fixture curation or seam testing; they are absent from normal product discovery, absent from read-only sidecars, and still route mutations through the owning command/session layer. They are not public product capabilities and must not occupy `graph.*`, `session.*`, or `workspace.*` names. Retired public names are quarantined in `src/rpc/README.md` §Names absent from current public RPC and must not re-enter product discovery. Depends on: D5-L, D6-L, D10-L, D16-L. Supersedes: the heavier “unified read gateway” mental model, vague `elicitation.*` / `command.*` public families, and any discovery/dispatch split where a surface describes methods it rejects. - **D23-L — Transport modes, operational modes, agent roles, strategies, and lenses are separate axes.** TUI, RPC, print, and web are transport modes: ways of driving or observing the same Brunch host through Pi/Brunch harness seams. Operational modes are top-level authority/tooling postures such as `elicit` and future `execute`. Agent roles are active workers within an operational mode (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor). Strategies are interaction plans; lenses are narrower interpretive/extraction/review framings. M1 print mode is therefore only a transport proof-of-life: it boots through the same host/coordinator, renders a snapshot of product-shaped state, and exits without running an agent turn. A future single-turn headless print run is deferred until runtime bundle selection/defaults are explicit. Depends on: D1-L, D5-L, D19-L, D21-L, D40-L. Supersedes: overloading “mode” to mean both transport and agent strategy, or using “agent mode” for role/preset/lens interchangeably. - **D33-L — Transport connections are client attachments, not Brunch sessions.** A Brunch session is a durable linear Pi JSONL transcript bound to exactly one spec; WebSocket connections, stdio streams, TUI instances, and browser tabs are ephemeral presentation attachments to product resources. Session-specific RPC methods should name their target spec/session explicitly or operate through an explicit client attachment; they must not infer durable session identity merely from the transport connection. `.brunch/workspace.json` remains launch/default acceleration, not concurrency authority. During the POC, Brunch targets a one-writer/many-observer local model: one interactive driver (typically TUI/agent) may write while web clients attach read-only for visual projections. Depends on: D5-L, D10-L, D11-L, D19-L, D21-L, D24-L. Supersedes: treating `/rpc`, a WebSocket, or workspace default state as the active session itself. @@ -282,7 +283,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/.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/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.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 readiness-grade 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 | +| I30-L | Elicitor post-exchange capture only commits high-confidence extractive facts, concrete reconciliation needs, and justified spec readiness-grade updates; low-confidence implications remain in structured-exchange preface/question material and do not become graph truth until clarified, accepted, or explicitly escalated. | partially covered (`src/graph/capture/structured-response.test.ts` accepts only directly labeled text facts for the current tracer, rejects implication-only prose as `no_capture`, preserves structural diagnostics, and `src/probes/capture-response-to-graph-proof.test.ts` proves public RPC response capture into selected-spec graph truth; reconciliation-needs and readiness-grade capture remain planned) | D18-L, D47-L; A22-L | | I31-L | `readiness_grade` is a forward gate, not a workflow location or kind whitelist: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable, and the `CommandExecutor` must not reject a graph node solely because its kind belongs to a later readiness band. All grade mutations route through `CommandExecutor` and carry audit through the change log. | partially covered (`createSpec` / `getSpec` / `updateReadinessGrade` command tests cover storage and mutation audit; `src/agents/compose.test.ts` covers prompt-resource grade gates rejecting illegal pinned commitment selections and filtering AUTO availability; kind-vs-grade write permissiveness remains planned with graph-code/readiness-band work) | D20-L, D45-L, D64-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 `session.submitExchangeResponse`, and the deterministic permutation run produces linear Pi JSONL whose structured exchange projection preserves the same prompt/answer/status/comment artifacts as the equivalent TUI structured-exchange path. | covered for deterministic FE-744 parity under canonical session method names (`session.triggerExchange`, `session.pendingExchange`, `session.submitExchangeResponse`, `session.exchanges`): `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/exchange parity assertions. | D5-L, D48-L, D49-L; I10-L, I13-L, I21-L, I23-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 | @@ -292,7 +293,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | 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 | | I38-L | Every Brunch prompt-resource manifest injected for an agent turn is generated from projected runtime state and spec/workspace gates: listed resources are Brunch-owned, readable under the active tool policy, legal for the current `(op_mode × goal × strategy × lens)` / grade / agent allow-list, and off-list resources are not advertised as available. AUTO axes never list illegal choices; pinned axes point to the pinned resource. | covered for current P0 manifest families (`src/agents/compose.test.ts` covers default header/context/manifest output, AUTO grade/allow-list filtering, pinned singleton resources, illegal pinned grade rejection, and readable `src/agents/` locations; `src/.pi/__tests__/prompting.test.ts` covers the explicit shell `before_agent_start` product path appending `agents/compose()` output from transcript-projected runtime state and no legacy composer import/resource discovery. Probe fitness may still track whether the agent reads selected resources before use.) | D39-L, D40-L, D58-L, D59-L | | I39-L | Every graph node in a spec has exactly one stable projected human reference code derived from `kind` + `kind_ordinal`; `(spec_id, plane, kind, kind_ordinal)` is unique; ordinals are monotonic per `(spec_id, plane, kind)` and are not reused after deletion or supersession. | partially covered (`graph-tool-resilience` added `nodes.kind_ordinal`, `node_kind_counters`, DB uniqueness, CommandExecutor allocation for single-node/batch writes, rollback protection, `GraphNode.kindOrdinal` row mapping, globally unique 1–3 letter labels with readiness-band metadata, projected-code parsing, selected-spec adapter resolution before `CommandExecutor`, code-only `commit_graph` / `read_graph` schemas, and code-primary prompt/tool rendering; remaining slice still needs deletion/supersession no-reuse coverage) | D54-L, D62-L; I1-L, I11-L | -| I40-L | Accepted graph nodes and edges use only `basis ∈ explicit | implicit`; review-set approval and direct user statements produce `explicit`, `propose-graph` concept-level materialization produces `implicit`, and the mutation path is recoverable from `change_log` rather than from a persisted basis enum value such as `accepted_review_set`. | partially covered (`graph-tool-resilience` replaced the persisted basis enum with `explicit | implicit`, made `commitGraph` apply one batch approval basis to all created nodes/edges, made single-node `createNode` reject retired basis values before LSN/counter/node/change-log allocation, made `propose-graph` adapter commits implicit, made review-set translation explicit, rejected retired `accepted_review_set`, and records `change_log.operation` independently; remaining capture/review-cycle slices still need direct user/capture assignment coverage) | D26-L, D27-L, D53-L, D63-L | +| I40-L | Accepted graph nodes and edges use only `basis ∈ explicit | implicit`; review-set approval and direct user statements produce `explicit`, `propose-graph` concept-level materialization produces `implicit`, and the mutation path is recoverable from `change_log` rather than from a persisted basis enum value such as `accepted_review_set`. | partially covered (`graph-tool-resilience` replaced the persisted basis enum with `explicit | implicit`, made `commitGraph` apply one batch approval basis to all created nodes/edges, made single-node `createNode` reject retired basis values before LSN/counter/node/change-log allocation, made `propose-graph` adapter commits implicit, made review-set translation explicit, rejected retired `accepted_review_set`, and records `change_log.operation` independently; `capture-response-to-graph` proves direct structured text responses commit explicit-basis graph nodes through `CommandExecutor`; remaining proof is full review-cycle acceptance) | D26-L, D27-L, D53-L, D63-L | | I41-L | Same-spec `supersession` edges form an acyclic directed graph; every edge-creation path validates proposed supersession edges together with existing supersession edges before committing. | covered (`command-executor/commit-graph-batch.test.ts` rejects existing-cycle closure, intra-batch cycles, and mixed existing+batch cycles through the shared dry-run/commit planner before batch writes; rejected cycles roll back or avoid batch nodes/edges/change_log; acyclic supersession commits remain covered by snapshot/CommandExecutor success paths) | D51-L, D53-L; I34-L | ## Future Direction Register @@ -620,7 +621,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I26-L | Structured-exchange schema tests prove the acknowledged Zod seam parses and exports JSON Schema; future M4 architectural tests should grep/import-audit schema libraries and Drizzle row-schema derivation boundaries. | | I28-L | Inner — TypeBox schema validation of [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) shape; deterministic anchor-rendering unit tests (same branch + same config → same header bytes). Middle (M9) — compaction round-trip property tests across all configured anchors and selection rules; fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer (M9) — long-horizon adversarial fixture confirms session binding, latest runtime state, latest establishment offer, in-flight side-task results, and unresolved staleness hints remain agent-intelligible post-compaction. | | I29-L | Inner — argv-shape tests for the `subagent` tool prove every spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent `--tools`/`--extension`/`--models`/`--append-system-prompt` set; TypeBox schema validation of `src/.pi/extensions/subagents/agents/*.md` frontmatter and `src/.pi/extensions/subagents/config.json`. Middle — isolation audit (no ambient `.pi/` resources reachable inside the subprocess; tool-allowlist conformance per starter agent; parent `CommandExecutor`/Brunch RPC handlers absent from subprocess environment). Outer — probe-driven proposal-generation runs invoking scout/researcher/graph-reader confirm grounding inputs flow through subagent outputs into review-set proposals without bypassing primary authority. | -| I30-L | M5 post-exchange capture fixtures: compare committed graph facts, reconciliation needs, and preface-only interpretations against transcript evidence; known ambiguous exchanges must not silently become graph truth. | +| I30-L | FE-807 covers the current labeled-text response tracer: committed graph facts are compared against transcript evidence and implication-only prose returns `no_capture`. Future capture fixtures still need reconciliation-need and readiness-grade cases plus broader LLM-quality comparisons against preface-only interpretations. | | I31-L | Spec-row command tests for grade updates plus prompt/tool-policy tests proving grade gates unlock later actions without disabling gathering/refinement; graph write tests proving later-band node kinds are not rejected solely because the current spec grade is lower. Card 1 covers the CommandExecutor grade-write path; prompt/tool-policy tests remain with M5. | | 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 session 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. | @@ -628,7 +629,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | 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. | | I38-L | `agents-composition-layer` inner tests: given projected runtime states and spec grades, compose emits manifests whose goal/strategy/lens/method resources are legal, Brunch-owned, readable, and filtered by the agent allow-list; AUTO axes list only legal choices and pinned axes point to their selected resource. Middle/outer probes may track whether the model actually reads the selected resource before applying it as fitness, not as an inner-loop gate. | | I39-L | `graph-tool-resilience` CommandExecutor/adapter/context tests: counter rows allocate monotonic per-kind ordinals in multi-node batches, rollback does not persist failed ordinals/counter rows, DB constraints reject duplicate `(spec_id, plane, kind, kind_ordinal)`, projected-code metadata is unique and parses by longest prefix, existing-code refs resolve inside the selected spec, and prompt/tool renderers use codes as primary handles. Remaining proof: deletion/supersession no-reuse. | -| I40-L | `graph-tool-resilience` CommandExecutor/adapter tests: `commitGraph` applies one batch basis to all created nodes/edges, single-node `createNode` rejects retired basis values before LSN/counter/node/change-log allocation, `propose-graph` adapter commits use `implicit`, review-set translation uses `explicit`, retired `accepted_review_set` is rejected, and `change_log.operation` remains independent of basis. Remaining proof: capture/direct-user writes and full review-cycle acceptance path. | +| I40-L | `graph-tool-resilience` CommandExecutor/adapter tests: `commitGraph` applies one batch basis to all created nodes/edges, single-node `createNode` rejects retired basis values before LSN/counter/node/change-log allocation, `propose-graph` adapter commits use `implicit`, review-set translation uses `explicit`, retired `accepted_review_set` is rejected, and `change_log.operation` remains independent of basis. FE-807 adds direct structured text response capture with `basis: explicit`. Remaining proof: full review-cycle acceptance path. | | I41-L | `graph-tool-resilience` CommandExecutor tests reject supersession cycles across existing edges, intra-batch edges, and mixed existing+batch edges, including rollback of batch nodes/edges/change_log; existing acyclic supersession paths still commit. | ### Design Notes @@ -636,7 +637,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` - **Prompt-resource manifests before eager prompt injection.** For goal, strategy, lens, and method guidance, prefer a deterministic per-turn manifest plus agent-driven `read` loading over a Brunch state machine that selects and concatenates large semantic prompt bodies. Inner-loop tests prove manifest legality and filtering; behavioral probes judge whether the agent loads and applies the right resource. - **Deterministic before generative.** Probe runs should prefer deterministic or tightly scripted paths before relying on LLM persona variance. Generative/adversarial probes come after the transcript substrate is trusted. Retired M1 scripted captures proved the early transport/projection substrate on then-current terms, but tuple-shaped FE-744 public-RPC probes are the current evidence path. - **Public RPC parity before LLM quality.** FE-744's product proof uses a deterministic dummy elicitor rather than a real LLM: the point is to prove Brunch's public RPC contract, assistant-first turn model, structured-exchange prompt/read/submit lifecycle, current structured-exchange permutations, JSONL/projection parity, and reviewable probe artifacts. The canonical method names live in [`src/rpc/README.md`](file:///Users/lunelson/Code/hashintel/brunch-next/src/rpc/README.md); current code and probes should use those names only. LLM elicitation quality and coherent ten-turn progress remain outer-loop generative fixture concerns after the transport/turn substrate is trustworthy. -- **Capture analysis before graph persistence.** `capture_*` ANALYSIS is the transcript-native bridge for reviewing likely graph changes before graph persistence or before comparing later graph mutations against transcript evidence. The landed schema layer defines only the checked minimum capture details and rejects graph payloads; richer analysis payloads and shared rendering components still require a separate design pass before runtime implementation. +- **Capture analysis stays distinct from response-capture writes.** `capture_*` ANALYSIS is the transcript-native bridge for reviewing likely graph changes before persistence or before comparing later graph mutations against transcript evidence; it is not the FE-807 synchronous response-capture command path. The landed schema layer defines only the checked minimum capture details and rejects graph payloads; richer analysis payloads and shared rendering components still require a separate design pass before runtime implementation. - **Projection handlers are oracles, not stores.** Read/subscription tests should prove handlers reconstruct truth from Brunch-supported linear Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log; they should not introduce a canonical view-store just for testing. - **Behavioral quality boundary.** Inner/middle loops prove structural validity, durable state, invariants, and expected graph/property coverage. “Good interview”, “good question”, and “coherent UX feel” remain outer-loop checklist/generative-fixture judgments until enough examples justify sharper metrics. - **Subscriptions are scoped for the POC.** Initial subscription oracles should prove initial snapshot plus ordered live updates by invalidating/refetching canonical projection handlers rather than introducing a view store. Reconnect/resume semantics are acknowledged but deferred unless a frontier explicitly depends on them. diff --git a/memory/cards/tooling--worktree-command-ux.md b/memory/cards/tooling--worktree-command-ux.md new file mode 100644 index 000000000..323fcbb9c --- /dev/null +++ b/memory/cards/tooling--worktree-command-ux.md @@ -0,0 +1,171 @@ +# Worktree command UX hardening + +Frontier: n/a +Status: active +Mode: chain +Created: 2026-06-05 + +## Orientation + +- Containing seam: project-local direct-Pi developer tooling in `.pi/extensions/worktree/index.ts`, not Brunch product runtime code. +- Relevant frontier item: n/a. This is a tooling follow-up to commits `ab562d64` and `f6ee3104`; it should stay outside `memory/PLAN.md` unless the user promotes developer-workflow tooling to a frontier. +- Volatile handoff state: no `HANDOFF.md`; worktree is clean except the unrelated active `memory/cards/dev-seed-fixtures--curation-loop.md` scope file, which this slice must not touch. +- Main open risk: slash-command UX can drift from Brunch's established namespacing convention or accidentally preserve old aliases; this local tooling is still under free-rewrite posture, so make the new command names canonical. + +Posture: proving (inherited from project default; no containing PLAN frontier). + +Cross-cutting obligations this chain carries: + +- Preserve D39-L's tooling exception: root `.pi/extensions/worktree/index.ts` is direct-Pi developer convenience only and must not enter `src/.pi/pi-extension-shell.ts` or the sealed Brunch Pi Profile. +- Preserve worktree safety invariants from the landed extension: create from caller `HEAD`, warn on dirty caller state, preserve old session files, and never delete/prune worktrees. +- Use the existing Brunch command namespace pattern as the reference: `src/.pi/extensions/commands.ts` registers literal command names like `brunch:switch`; its file comment documents that Pi parses slash command names up to the first whitespace and passes colons through verbatim. + +## Card 1 — Namespace worktree slash commands + +Status: done +Weight: light + +### Objective + +Make `/worktree:create` and `/worktree:switch` the canonical slash commands for the project-local worktree extension. + +### Acceptance Criteria + +```pseudo +command registration +├── registers `worktree:create` for sibling worktree creation +├── registers `worktree:switch` for session relocation +├── does not register `/create-worktree` or `/switch-worktree` aliases +└── follows the `src/.pi/extensions/commands.ts` pattern: literal command constants containing `:` + +staged command text +├── `createSiblingWorktree` stages `/worktree:switch ` +├── `switch_worktree` stages `/worktree:switch ` +├── tool descriptions / prompt snippets name `/worktree:switch` +└── test expectations no longer mention old slash-command names except as negative assertions + +canonical docs +└── `memory/SPEC.md` D39-L tooling exception, if it names slash commands, names `/worktree:create` and `/worktree:switch` +``` + +### Verification Approach + +- Inner: `npm test -- src/.pi/__tests__/project-worktree-extension.test.ts` — proves registration, editor staging, and command-text changes. +- Inner: `npx oxlint .pi/extensions/worktree/index.ts src/.pi/__tests__/project-worktree-extension.test.ts` and `npx oxfmt --check ...` — proves touched files remain linted/formatted. + +### Cross-cutting obligations + +- Keep tool names `create_worktree` and `switch_worktree`; this card only renames slash commands. +- Do not add compatibility aliases for old slash commands unless the user explicitly asks. +- Do not modify Brunch product command registration under `src/.pi/extensions/commands.ts`; use it only as a pattern reference. + +### Assumption dependency + +None — Pi colon command parsing is already used by `src/.pi/extensions/commands.ts` and covered by existing Brunch command practice. + +### Expected touched paths (tentative) + +```pseudo +.pi/extensions/ +└── worktree/ + └── index.ts ~ + +src/.pi/__tests__/ +└── project-worktree-extension.test.ts ~ + +memory/ +└── SPEC.md ? +``` + +Done 2026-06-05: + +- Renamed canonical slash commands to `/worktree:switch` and `/worktree:create` while preserving tool names `switch_worktree` and `create_worktree`. +- Updated create/switch editor staging and tool descriptions/prompt snippets to name `/worktree:switch`. +- Reconciled D39-L tooling exception in `memory/SPEC.md` to name the namespaced slash commands. + +### Promotion checklist + +- [ ] Does this change a requirement? No — this is local tooling UX naming. +- [ ] Does this create, retire, or invalidate an assumption? No. +- [ ] Does this slice depend on an unvalidated high-impact assumption? No. +- [ ] Does this make or reverse a non-trivial design decision? No — follows existing colon namespace pattern. +- [ ] Does this establish a new seam-level invariant? No. +- [ ] Does this change a frontier-level cross-cutting obligation or verification architecture layer? No. +- [ ] Does it cross more than two major seams? No. +- [ ] Is this the first touch in an unfamiliar seam from a fresh thread? No. +- [ ] Can you not name the containing seam or current rationale from the live docs? No. + +## Card 2 — Offer existing worktrees from no-arg switch + +Status: next +Weight: light + +### Objective + +Make `/worktree:switch` without a path open an interactive selector over existing sibling/current-repo worktrees. + +### Acceptance Criteria + +```pseudo +no-arg switch discovery +├── `/worktree:switch ` keeps the existing direct validation + confirm + relocation behavior +├── `/worktree:switch` runs `git worktree list --porcelain` from the caller cwd +├── parses worktree entries into path plus branch/detached display metadata +├── excludes the caller worktree root from selectable targets +├── notifies when the caller cwd is not in a git repository +├── notifies when there are no other worktrees +└── cancels cleanly when the user dismisses the selector + +interactive selection +├── uses `ctx.ui.select` so the choice appears in Pi's overlay/dialog UI +├── labels options with enough context to distinguish path and branch/detached state +├── passes the selected path through the existing `runSwitchWorktree` validation path +└── keeps the existing confirmation before session relocation + +tests +├── covers porcelain parsing for branch and detached entries +├── covers exclusion of the current worktree +├── covers selector cancellation +└── covers selecting an existing worktree and reaching the switch path +``` + +### Verification Approach + +- Inner: helper tests for `git worktree list --porcelain` parsing and option filtering. +- Inner: command-handler/unit tests with fake `ctx.ui.select` — proves no-arg behavior, cancellation, and selected-path handoff. +- Middle: temp-git smoke in `src/.pi/__tests__/project-worktree-extension.test.ts` if practical — proves discovery sees linked worktrees created by real git. +- Gate: `npm run verify` before commit, scoped failures outside touched files reported rather than fixed. + +### Cross-cutting obligations + +- Keep relocation itself on the existing validated/confirmed `runSwitchWorktree` path; the selector is only target choice, not a bypass. +- Do not build worktree list/delete/prune management. +- Do not auto-switch when only one alternative exists; still show/select or otherwise require explicit user action. +- Do not use this command as a Brunch product spec/session switcher; it is direct-Pi cwd/session relocation only. + +### Assumption dependency + +None — this depends only on Git's stable porcelain worktree listing and existing Pi `ctx.ui.select` behavior. + +### Expected touched paths (tentative) + +```pseudo +.pi/extensions/ +└── worktree/ + └── index.ts ~ + +src/.pi/__tests__/ +└── project-worktree-extension.test.ts ~ +``` + +### Promotion checklist + +- [ ] Does this change a requirement? No — this hardens local tooling UX. +- [ ] Does this create, retire, or invalidate an assumption? No. +- [ ] Does this slice depend on an unvalidated high-impact assumption? No. +- [ ] Does this make or reverse a non-trivial design decision? No. +- [ ] Does this establish a new seam-level invariant? No. +- [ ] Does this change a frontier-level cross-cutting obligation or verification architecture layer? No. +- [ ] Does it cross more than two major seams? No. +- [ ] Is this the first touch in an unfamiliar seam from a fresh thread? No. +- [ ] Can you not name the containing seam or current rationale from the live docs? No. diff --git a/src/.pi/__tests__/extension-registry.test.ts b/src/.pi/__tests__/extension-registry.test.ts index a02673700..703431b5e 100644 --- a/src/.pi/__tests__/extension-registry.test.ts +++ b/src/.pi/__tests__/extension-registry.test.ts @@ -87,7 +87,6 @@ describe('Brunch explicit Pi extension registry', () => { 'tool_call', 'user_bash', 'before_agent_start', - 'before_agent_start', 'session_start', ]); diff --git a/src/.pi/__tests__/operational-mode.test.ts b/src/.pi/__tests__/operational-mode.test.ts index 47336657f..d392026f6 100644 --- a/src/.pi/__tests__/operational-mode.test.ts +++ b/src/.pi/__tests__/operational-mode.test.ts @@ -138,6 +138,9 @@ describe('Brunch agent runtime-state projection', () => { 'ls', 'present_question', 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', 'read_graph', 'commit_graph', 'bash', @@ -164,7 +167,18 @@ describe('Brunch agent runtime-state projection', () => { ); expect(activeTools).toEqual([ - ['read', 'grep', 'find', 'ls', 'present_question', 'present_options', 'read_graph'], + [ + 'read', + 'grep', + 'find', + 'ls', + 'present_question', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + ], ]); expect(promptResult).toBeUndefined(); for (const toolName of ['bash', 'edit', 'write']) { diff --git a/src/.pi/__tests__/project-worktree-extension.test.ts b/src/.pi/__tests__/project-worktree-extension.test.ts new file mode 100644 index 000000000..ab1ff88d6 --- /dev/null +++ b/src/.pi/__tests__/project-worktree-extension.test.ts @@ -0,0 +1,397 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { basename, dirname, join } from 'node:path'; +import { promisify } from 'node:util'; + +import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; + +import worktreeExtension, { + cleanForkedSessionHeader, + createRelocatedSession, + createSiblingWorktree, + planSiblingWorktree, + resolveSwitchTarget, + runSwitchWorktree, + validateGitWorktree, + WORKTREE_CREATE_COMMAND, + WORKTREE_CREATE_TOOL, + WORKTREE_SWITCH_COMMAND, + WORKTREE_SWITCH_TOOL, +} from '../../../.pi/extensions/worktree/index.js'; + +const execFile = promisify(execFileCallback); + +describe('project-local worktree Pi extension', () => { + it('registers the auto-discovered command and LLM staging tool', () => { + const recording = createRecordingApi(); + + worktreeExtension(recording.api); + + expect(WORKTREE_SWITCH_COMMAND).toBe('worktree:switch'); + expect(WORKTREE_CREATE_COMMAND).toBe('worktree:create'); + expect(recording.commandNames).toEqual(['worktree:switch', 'worktree:create']); + expect(recording.commandNames).not.toContain('switch-worktree'); + expect(recording.commandNames).not.toContain('create-worktree'); + expect(recording.toolNames).toEqual([WORKTREE_SWITCH_TOOL, WORKTREE_CREATE_TOOL]); + expect(recording.tools[0]?.promptGuidelines).toContain( + 'Call switch_worktree only after the user explicitly asks to move this Pi session to another git worktree.', + ); + expect(recording.tools[0]?.description).toContain('/worktree:switch '); + expect(recording.tools[0]?.description).not.toContain('/switch-worktree'); + expect(recording.tools[1]?.promptSnippet).toContain('/worktree:switch '); + expect(recording.tools[1]?.promptSnippet).not.toContain('/switch-worktree'); + }); + + it('normalizes targets against the caller cwd and validates git worktrees', async () => { + await withTempDir(async (dir) => { + const repo = join(dir, 'repo'); + const file = join(repo, 'file.txt'); + await git(dir, 'init', 'repo'); + await writeFile(file, 'tracked\n'); + await git(repo, 'add', 'file.txt'); + await git(repo, '-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'initial'); + + const linked = join(dir, 'repo-linked'); + await git(repo, 'worktree', 'add', linked, 'HEAD'); + + expect(resolveSwitchTarget('repo-linked', dir)).toBe(linked); + await expect(validateGitWorktree(repo)).resolves.toEqual({ ok: true, cwd: repo }); + await expect(validateGitWorktree(linked)).resolves.toEqual({ ok: true, cwd: linked }); + await expect(validateGitWorktree(join(dir, 'missing'))).resolves.toEqual({ + ok: false, + reason: 'missing', + path: join(dir, 'missing'), + }); + await expect(validateGitWorktree(file)).resolves.toEqual({ + ok: false, + reason: 'not-directory', + path: file, + }); + + const nongit = join(dir, 'plain'); + await mkdir(nongit); + await git(dir, 'init', '--bare', 'bare.git'); + await expect(validateGitWorktree(nongit)).resolves.toEqual({ + ok: false, + reason: 'not-git-worktree', + path: nongit, + }); + await expect(validateGitWorktree(join(dir, 'bare.git'))).resolves.toEqual({ + ok: false, + reason: 'bare-repository', + path: join(dir, 'bare.git'), + }); + }); + }); + + it('forks the current session into the target cwd without retaining parent-session metadata', async () => { + await withTempDir(async (dir) => { + const sourceSession = join(dir, 'source.jsonl'); + const target = join(dir, 'target'); + const sessionDir = join(dir, 'sessions'); + await git(dir, 'init', 'target'); + await writeFile( + sourceSession, + [ + JSON.stringify({ + type: 'session', + version: 3, + id: 'source-session', + timestamp: '2026-06-05T00:00:00.000Z', + cwd: dir, + parentSession: '/old-parent.jsonl', + }), + JSON.stringify({ + type: 'message', + id: 'm1', + parentId: null, + timestamp: '2026-06-05T00:00:01.000Z', + message: { role: 'assistant', content: [{ type: 'text', text: 'hello' }], timestamp: 0 }, + }), + '', + ].join('\n'), + ); + + const relocated = await createRelocatedSession(sourceSession, target, sessionDir); + const relocatedContent = await readFile(relocated, 'utf8'); + const relocatedHeader = JSON.parse(relocatedContent.split('\n')[0] ?? '{}') as Record; + + expect(relocatedHeader.cwd).toBe(target); + expect(relocatedHeader.parentSession).toBeUndefined(); + await expect(stat(sourceSession)).resolves.toBeTruthy(); + }); + }); + + it('confirms before switching and sends the continuation through the replacement context', async () => { + await withTempDir(async (dir) => { + const target = join(dir, 'target'); + const sourceSession = join(dir, 'source.jsonl'); + const sessionDir = join(dir, 'sessions'); + await git(dir, 'init', 'target'); + await writeFile( + sourceSession, + `${JSON.stringify({ type: 'session', version: 3, id: 's1', timestamp: '2026-06-05T00:00:00.000Z', cwd: dir })}\n${JSON.stringify({ type: 'message', id: 'm1', parentId: null, timestamp: '2026-06-05T00:00:01.000Z', message: { role: 'assistant', content: [{ type: 'text', text: 'hello' }], timestamp: 0 } })}\n`, + ); + const ctx = createSwitchContext({ cwd: dir, sourceSession, sessionDir, confirm: true }); + + await runSwitchWorktree(target, ctx, { sessionDir }); + + expect(ctx.confirmations).toHaveLength(1); + expect(ctx.switchedSessionFile).toContain(sessionDir); + expect(ctx.replacementMessages).toEqual([`Continue in the relocated Pi session from cwd: ${target}`]); + expect(ctx.notifications.at(-1)).toEqual({ + message: `Relocated Pi session to ${target}`, + type: 'info', + }); + }); + }); + + it('removes parentSession only from the session header line', async () => { + await withTempDir(async (dir) => { + const session = join(dir, 'session.jsonl'); + await writeFile( + session, + `${JSON.stringify({ type: 'session', id: 's1', timestamp: '2026-06-05T00:00:00.000Z', cwd: dir, parentSession: 'old' })}\n${JSON.stringify({ type: 'custom', id: 'c1', parentId: null, timestamp: '2026-06-05T00:00:01.000Z', data: { parentSession: 'kept' } })}\n`, + ); + + await cleanForkedSessionHeader(session); + + const [headerLine, customLine] = (await readFile(session, 'utf8')).trimEnd().split('\n'); + expect(JSON.parse(headerLine ?? '{}')).not.toHaveProperty('parentSession'); + expect(JSON.parse(customLine ?? '{}')).toHaveProperty('data.parentSession', 'kept'); + }); + }); + + it('plans sibling defaults from the caller worktree root and skips path and branch collisions', async () => { + await withTempDir(async (dir) => { + const repo = join(dir, 'repo'); + await initRepo(repo); + await mkdir(join(dir, 'repo-alpha')); + await git(repo, 'branch', 'repo-beta'); + + await expect( + planSiblingWorktree({ + sourceRoot: repo, + branchExists: async (branch) => branch === 'repo-beta', + pathExists: async (path) => path === join(dir, 'repo-alpha'), + greekWords: ['alpha', 'beta', 'gamma'], + chooseStartIndex: () => 0, + }), + ).resolves.toEqual({ + path: join(dir, 'repo-gamma'), + branch: 'repo-gamma', + attempted: ['repo-alpha', 'repo-beta', 'repo-gamma'], + }); + }); + }); + + it('creates sibling worktrees from caller HEAD in main and linked worktrees', async () => { + await withTempDir(async (dir) => { + const main = join(dir, 'repo'); + await initRepo(main); + const mainHead = await gitOutput(main, 'rev-parse', 'HEAD'); + + const linked = join(dir, 'repo-linked'); + await git(main, 'worktree', 'add', '-b', 'linked', linked, 'HEAD'); + await writeFile(join(linked, 'linked.txt'), 'linked\n'); + await git(linked, 'add', 'linked.txt'); + await git( + linked, + '-c', + 'user.email=test@example.com', + '-c', + 'user.name=Test', + 'commit', + '-m', + 'linked', + ); + const linkedHead = await gitOutput(linked, 'rev-parse', 'HEAD'); + + const mainCtx = createWorktreeCreationContext(main); + const mainResult = await createSiblingWorktree(mainCtx, { + greekWords: ['alpha'], + chooseStartIndex: () => 0, + }); + if (mainResult.status !== 'created') throw new Error(mainResult.reason); + expect(mainResult).toMatchObject({ + status: 'created', + sourceCommit: mainHead, + branch: 'repo-alpha', + path: join(dirname(mainResult.sourceRoot), 'repo-alpha'), + }); + expect(await gitOutput(mainResult.path, 'rev-parse', 'HEAD')).toBe(mainHead); + expect(mainCtx.editorText).toBe(`/worktree:switch ${mainResult.path}`); + + const linkedCtx = createWorktreeCreationContext(linked); + const linkedResult = await createSiblingWorktree(linkedCtx, { + greekWords: ['beta'], + chooseStartIndex: () => 0, + }); + if (linkedResult.status !== 'created') throw new Error(linkedResult.reason); + expect(linkedResult).toMatchObject({ + status: 'created', + sourceCommit: linkedHead, + branch: 'repo-linked-beta', + path: join(dirname(linkedResult.sourceRoot), 'repo-linked-beta'), + }); + expect(await gitOutput(linkedResult.path, 'rev-parse', 'HEAD')).toBe(linkedHead); + expect(linkedCtx.editorText).toBe(`/worktree:switch ${linkedResult.path}`); + }); + }, 15000); + + it('warns when the caller worktree is dirty but still creates from committed HEAD', async () => { + await withTempDir(async (dir) => { + const repo = join(dir, 'repo'); + await initRepo(repo); + const head = await gitOutput(repo, 'rev-parse', 'HEAD'); + await writeFile(join(repo, 'dirty.txt'), 'not committed\n'); + const ctx = createWorktreeCreationContext(repo); + + const result = await createSiblingWorktree(ctx, { greekWords: ['delta'], chooseStartIndex: () => 0 }); + if (result.status !== 'created') throw new Error(result.reason); + + expect(result).toMatchObject({ + status: 'created', + sourceCommit: head, + dirty: true, + dirtyWarning: + 'Caller worktree has uncommitted changes; the new worktree was created from committed HEAD only.', + }); + expect(ctx.notifications).toContainEqual({ + message: + 'Caller worktree has uncommitted changes; the new worktree was created from committed HEAD only.', + type: 'warning', + }); + await expect(stat(join(result.path, 'dirty.txt'))).rejects.toMatchObject({ code: 'ENOENT' }); + }); + }); +}); + +function createRecordingApi() { + const commandNames: string[] = []; + const toolNames: string[] = []; + const tools: Array<{ + name: string; + description?: string; + promptSnippet?: string; + promptGuidelines?: string[]; + }> = []; + const api = { + registerCommand(name: string) { + commandNames.push(name); + }, + registerTool(tool: { + name: string; + description?: string; + promptSnippet?: string; + promptGuidelines?: string[]; + }) { + toolNames.push(tool.name); + tools.push(tool); + }, + }; + return { api: api as never as ExtensionAPI, commandNames, toolNames, tools }; +} + +function createSwitchContext({ + cwd, + sourceSession, + sessionDir, + confirm, +}: { + cwd: string; + sourceSession: string; + sessionDir: string; + confirm: boolean; +}) { + const confirmations: string[] = []; + const notifications: Array<{ message: string; type: 'info' | 'warning' | 'error' | undefined }> = []; + const replacementMessages: string[] = []; + const ctx = { + cwd, + hasUI: true, + ui: { + confirm: async (title: string, message: string) => { + confirmations.push(`${title}\n${message}`); + return confirm; + }, + notify: (message: string, type?: 'info' | 'warning' | 'error') => { + notifications.push({ message, type }); + }, + setEditorText() {}, + }, + sessionManager: { + getSessionFile: () => sourceSession, + getSessionDir: () => sessionDir, + }, + switchSession: async ( + sessionFile: string, + options: { + withSession?: (replacementCtx: { + sendUserMessage: (message: string) => Promise; + ui: { notify: (message: string, type?: 'info' | 'warning' | 'error') => void }; + }) => Promise; + }, + ) => { + ctx.switchedSessionFile = sessionFile; + await options.withSession?.({ + sendUserMessage: async (message: string) => { + replacementMessages.push(message); + }, + ui: ctx.ui, + }); + return { cancelled: false }; + }, + switchedSessionFile: undefined as string | undefined, + confirmations, + notifications, + replacementMessages, + }; + return ctx as typeof ctx & ExtensionCommandContext; +} + +function createWorktreeCreationContext(cwd: string) { + const notifications: Array<{ message: string; type: 'info' | 'warning' | 'error' | undefined }> = []; + const ctx = { + cwd, + hasUI: true, + editorText: undefined as string | undefined, + ui: { + notify: (message: string, type?: 'info' | 'warning' | 'error') => { + notifications.push({ message, type }); + }, + setEditorText: (text: string) => { + ctx.editorText = text; + }, + }, + notifications, + }; + return ctx; +} + +async function withTempDir(run: (dir: string) => Promise): Promise { + const dir = await mkdtemp(join(tmpdir(), 'brunch-pi-worktree-')); + try { + await run(dir); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +async function initRepo(path: string): Promise { + await git(dirname(path), 'init', basename(path)); + await writeFile(join(path, 'tracked.txt'), 'tracked\n'); + await git(path, 'add', 'tracked.txt'); + await git(path, '-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'initial'); +} + +async function gitOutput(cwd: string, ...args: string[]): Promise { + const { stdout } = await execFile('git', args, { cwd }); + return stdout.trim(); +} + +async function git(cwd: string, ...args: string[]): Promise { + await execFile('git', args, { cwd }); +} diff --git a/src/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts index 24ba76fd2..9bcbb9ba1 100644 --- a/src/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -160,7 +160,7 @@ describe('Brunch prompt-pack topology', () => { events[event] = handler; }, getAllTools: () => - ['read', 'grep', 'bash', 'write', 'present_options'].map((name) => ({ + ['read', 'grep', 'bash', 'write', 'present_options', 'request_answer'].map((name) => ({ name, })), } as never, @@ -185,7 +185,7 @@ describe('Brunch prompt-pack topology', () => { systemPrompt: expect.stringContaining('- strategy: step-wise-disambiguate'), }); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining('- active tools: read, grep, present_options'), + systemPrompt: expect.stringContaining('- active tools: read, grep, present_options, request_answer'), }); expect(result).toMatchObject({ systemPrompt: expect.stringContaining('[Selected-spec graph context · design lens]'), @@ -292,9 +292,19 @@ describe('Brunch prompt-pack topology', () => { }, registerTool: (_tool: { name: string }) => {}, getAllTools: () => - ['read', 'grep', 'bash', 'edit', 'write', 'present_options', 'read_graph', 'commit_graph'].map( - (name) => ({ name }), - ), + [ + 'read', + 'grep', + 'bash', + 'edit', + 'write', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + 'commit_graph', + ].map((name) => ({ name })), setActiveTools: (tools: string[]) => activeTools.push(tools), }; registerBrunchOperationalModePolicy(pi as never); @@ -339,11 +349,53 @@ describe('Brunch prompt-pack topology', () => { expect(manager.entries[0]?.customType).toBe(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE); expect(activeTools).toEqual([ - ['read', 'grep', 'present_options', 'read_graph'], - ['read', 'grep', 'present_options', 'read_graph'], - ['read', 'grep', 'present_options', 'read_graph', 'commit_graph'], - ['read', 'grep', 'present_options', 'read_graph'], - ['read', 'grep', 'present_options', 'read_graph', 'commit_graph'], + [ + 'read', + 'grep', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + ], + [ + 'read', + 'grep', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + ], + [ + 'read', + 'grep', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + 'commit_graph', + ], + [ + 'read', + 'grep', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + ], + [ + 'read', + 'grep', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'read_graph', + 'commit_graph', + ], ]); expect(defaultPrompt).toMatchObject({ systemPrompt: expect.stringContaining('- strategy: auto'), @@ -353,7 +405,7 @@ describe('Brunch prompt-pack topology', () => { }); expect(defaultPrompt).toMatchObject({ systemPrompt: expect.stringContaining( - '- active tools: read, grep, present_options, read_graph, commit_graph', + '- active tools: read, grep, present_options, request_answer, request_choice, request_choices, read_graph, commit_graph', ), }); expect(defaultPrompt).toMatchObject({ @@ -491,7 +543,10 @@ describe('Brunch prompt-pack topology', () => { registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, - getAllTools: () => ['read', 'grep', 'present_options'].map((name) => ({ name })), + getAllTools: () => + ['read', 'grep', 'present_options', 'request_answer', 'request_choice', 'request_choices'].map( + (name) => ({ name }), + ), setActiveTools() {}, } as never); diff --git a/src/.pi/extensions/prompting.ts b/src/.pi/extensions/prompting.ts index 376eb2253..d398cef58 100644 --- a/src/.pi/extensions/prompting.ts +++ b/src/.pi/extensions/prompting.ts @@ -46,18 +46,12 @@ function projectState(ctx: BeforeAgentStartContextLike | undefined) { return projectBrunchAgentState(ctx?.sessionManager?.getEntries() ?? []); } -export function registerBrunchPrompting( - pi: ExtensionAPI, - promptContext: BrunchPromptContextProvider | undefined, -): void { +export function registerBrunchPrompting(pi: ExtensionAPI, promptContext: BrunchPromptContextProvider): void { if (!supportsPrompting(pi)) return; pi.on('before_agent_start', async (event, ctx) => { - if (!promptContext) { - throw new Error('Brunch prompting requires selected spec and workspace context.'); - } - const resolvedPromptContext = await resolvePromptContext(promptContext); + const state = projectState(ctx as BeforeAgentStartContextLike | undefined); const activeTools = typeof (pi as Partial).getAllTools === 'function' diff --git a/src/.pi/pi-extension-shell.ts b/src/.pi/pi-extension-shell.ts index 63bd27d9c..a44d665cc 100644 --- a/src/.pi/pi-extension-shell.ts +++ b/src/.pi/pi-extension-shell.ts @@ -105,18 +105,20 @@ export function createBrunchPiExtensionShell( ): ExtensionFactory { return async (pi) => { const graphMentionSource = options.graphMentionSource ?? FIXTURE_GRAPH_MENTION_SOURCE; - const extensions: readonly BrunchProductExtensionRegistrar[] = [ + const extensions: BrunchProductExtensionRegistrar[] = [ (api) => registerBrunchSessionBoundary(api, onSessionBoundary), (api) => registerBrunchChrome(api, chrome), registerBrunchBranchPolicyHandlers, registerBrunchOperationalModePolicy, - (api) => registerBrunchPrompting(api, options.promptContext), (api) => registerBrunchMentionAutocomplete(api, graphMentionSource), registerBrunchAlternatives, registerStructuredExchange, (api) => registerBrunchCommands(api, options), ...(options.graph ? [(api: ExtensionAPI) => registerBrunchGraph(api, options.graph!)] : []), ]; + if (options.promptContext) { + extensions.splice(4, 0, (api) => registerBrunchPrompting(api, options.promptContext!)); + } for (const registerExtension of extensions) { await registerExtension(pi); diff --git a/src/agents/state.test.ts b/src/agents/state.test.ts index dee965317..bdd9782d0 100644 --- a/src/agents/state.test.ts +++ b/src/agents/state.test.ts @@ -13,6 +13,9 @@ const registeredToolNames = [ 'write', 'present_question', 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', 'read_graph', 'commit_graph', ]; @@ -50,6 +53,9 @@ describe('agent posture policy', () => { expect(groundingTools).not.toContain('commit_graph'); expect(groundingTools).toContain('read_graph'); expect(groundingTools).not.toContain('bash'); + expect(groundingTools).toEqual( + expect.arrayContaining(['present_question', 'present_options', 'request_answer']), + ); expect(elicitationMethods).toContain('commit-graph'); expect(elicitationTools).toContain('commit_graph'); diff --git a/src/agents/state.ts b/src/agents/state.ts index 40f6ffe75..c555ac9fb 100644 --- a/src/agents/state.ts +++ b/src/agents/state.ts @@ -87,7 +87,13 @@ const METHOD_MIN_GRADE: Record = { }; const METHOD_TOOL_NAMES: Partial> = { - 'run-structured-exchange': ['present_question', 'present_options'], + 'run-structured-exchange': [ + 'present_question', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + ], 'read-snapshot': ['read_graph'], 'commit-graph': ['commit_graph'], }; diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index dc3db44ad..7d9e9a0cb 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -911,7 +911,19 @@ describe('Brunch TUI boot', () => { expect(registeredTools).toEqual(['read', 'grep', 'find', 'ls']); await events.session_start?.({} as never); - expect(activeTools).toEqual([['read', 'grep', 'find', 'ls', 'present_question', 'present_options']]); + expect(activeTools).toEqual([ + [ + 'read', + 'grep', + 'find', + 'ls', + 'present_question', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + ], + ]); await expect( Promise.resolve(events.before_agent_start?.({ systemPrompt: 'base' } as never)), ).resolves.toBeUndefined(); diff --git a/src/brunch.test.ts b/src/brunch.test.ts index c384a0c66..d1fd6be33 100644 --- a/src/brunch.test.ts +++ b/src/brunch.test.ts @@ -238,6 +238,30 @@ describe('Brunch CLI dispatch', () => { }); }); + it('gates dev RPC methods in CLI rpc mode behind BRUNCH_DEV_RPC=1', async () => { + const previous = process.env.BRUNCH_DEV_RPC; + const stdout = new PassThrough(); + const chunks = collectStream(stdout); + process.env.BRUNCH_DEV_RPC = '1'; + try { + const code = await runBrunchCli({ + argv: ['--mode=rpc'], + cwd: '/tmp/brunch-project', + coordinator: coordinator(), + stdin: rpcRequest('rpc.discover'), + stdout, + }); + + expect(code).toBe(0); + expect(JSON.stringify(JSON.parse(chunks.join('')))).toContain('dev.graph.commitGraph'); + } finally { + if (previous === undefined) { + delete process.env.BRUNCH_DEV_RPC; + } else { + process.env.BRUNCH_DEV_RPC = previous; + } + } + }); it('exposes matching print and RPC workspace snapshots from a real coordinator store', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-parity-')); await createWorkspaceSessionCoordinator({ cwd }).createSetupSession({ diff --git a/src/brunch.ts b/src/brunch.ts index e0a0b3ddb..c2f82e888 100644 --- a/src/brunch.ts +++ b/src/brunch.ts @@ -45,7 +45,12 @@ export async function runBrunchCli(options: BrunchCliOptions = {}): Promise { it('creates a missing database file and can reopen it idempotently', async () => { @@ -27,4 +29,67 @@ describe('createDb', () => { await rm(dir, { recursive: true, force: true }); } }); + + it('migrates a non-empty legacy graph database to kind ordinals and explicit basis', async () => { + const dir = await mkdtemp(join(tmpdir(), 'brunch-db-legacy-')); + const dbPath = join(dir, 'legacy.db'); + + try { + await createLegacy0000Database(dbPath); + + const db = createDb(dbPath); + const nodeRows = db.select().from(nodes).all(); + const edgeRows = db.select().from(edges).all(); + const counterRows = db.select().from(nodeKindCounters).all(); + + expect(nodeRows.map((row) => [row.kind, row.kind_ordinal, row.basis])).toEqual([ + ['goal', 1, 'explicit'], + ['goal', 2, 'explicit'], + ['requirement', 1, 'explicit'], + ]); + expect(edgeRows.map((row) => row.basis)).toEqual(['explicit']); + expect(counterRows.map((row) => [row.plane, row.kind, row.next_ordinal])).toEqual([ + ['intent', 'goal', 3], + ['intent', 'requirement', 2], + ]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); }); + +async function createLegacy0000Database(dbPath: string): Promise { + const migration = await readFile(new URL('../../drizzle/0000_deep_maria_hill.sql', import.meta.url)); + const sqlite = new Database(dbPath); + try { + sqlite.exec(migration.toString('utf8')); + sqlite.exec(` + INSERT INTO specs (id, name, slug, readiness_grade) + VALUES (1, 'Legacy spec', 'legacy-spec', 'grounding_onboarding'); + + INSERT INTO nodes ( + id, spec_id, plane, kind, title, body, basis, source, detail, created_at_lsn, updated_at_lsn + ) + VALUES + (1, 1, 'intent', 'goal', 'First goal', NULL, 'accepted_review_set', NULL, NULL, 0, 0), + (2, 1, 'intent', 'goal', 'Second goal', NULL, 'explicit', NULL, NULL, 0, 0), + (3, 1, 'intent', 'requirement', 'Requirement', NULL, 'accepted_review_set', NULL, NULL, 0, 0); + + INSERT INTO edges ( + id, spec_id, category, source_id, target_id, stance, basis, rationale, created_at_lsn, updated_at_lsn + ) + VALUES (1, 1, 'support', 1, 3, 'for', 'accepted_review_set', NULL, 0, 0); + + CREATE TABLE "__drizzle_migrations" ( + id SERIAL PRIMARY KEY, + hash text NOT NULL, + created_at numeric + ); + `); + sqlite + .prepare('INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)') + .run(createHash('sha256').update(migration).digest('hex'), 1780478757603); + } finally { + sqlite.close(); + } +} diff --git a/src/graph/README.md b/src/graph/README.md index 0baa92a65..6e5958e1c 100644 --- a/src/graph/README.md +++ b/src/graph/README.md @@ -15,6 +15,11 @@ SPEC decisions: D4-L, D20-L, D51-L, D52-L, D53-L, D54-L, D62-L, D63-L (`nodes[]` with batch refs, `edges[]` with batch/existing refs), not raw DB rows. `command-executor/commit-graph-batch.ts` owns the private shared planner used by both dry-run and commit before any batch writes occur. + +- **Capture translators** (`capture/`) — narrow, high-confidence structured + response translators that turn transcript-native answers into `commitGraph` + command input. They do not write DB rows directly and do not own session + projection. - **Readers / snapshot functions** (`snapshot.ts`) — graph projections at multiple detail levels: active-context and graph-truth overview, node neighborhood, selected-spec graph-code lookup, and open reconciliation needs. @@ -39,7 +44,7 @@ SPEC decisions: D4-L, D20-L, D51-L, D52-L, D53-L, D54-L, D62-L, D63-L ## Imported by - `.pi/extensions/graph/` — Pi tool adapters for `commit_graph` and `read_graph`. -- `rpc/` — future `graph.*` projection handlers and graph-adjacent state. +- `rpc/` — graph projection handlers and synchronous response-capture wiring. - `agents/contexts/` — future prompt context renderers. - `probes/` — graph proof drivers. @@ -72,6 +77,10 @@ graph/ dry-run/commit structural parity temporary endpoint graph for supersession acyclicity + capture/ + structured-response.ts + deterministic labeled-answer capture to explicit-basis commitGraph input + snapshot.ts getGraphOverview getNodeNeighborhood @@ -111,8 +120,9 @@ CommandExecutor ├─► .pi/extensions/graph │ agent tool adapter │ - ├─► rpc/ future graph handlers + ├─► rpc/ │ public product projections + │ session.submitExchangeResponse capture wiring │ └─► agents/contexts future renderers prompt context snapshots diff --git a/src/graph/capture/structured-response.test.ts b/src/graph/capture/structured-response.test.ts new file mode 100644 index 000000000..a64ee5900 --- /dev/null +++ b/src/graph/capture/structured-response.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from 'vitest'; + +import type { CommandExecutor } from '../command-executor.js'; +import type { CommitGraphInput, CommitGraphResult } from '../command-executor/commit-graph-types.js'; +import { captureStructuredResponseFacts } from './structured-response.js'; + +class RecordingExecutor { + readonly calls: CommitGraphInput[] = []; + + constructor(private readonly result: CommitGraphResult) {} + + commitGraph(input: CommitGraphInput): CommitGraphResult { + this.calls.push(input); + return this.result; + } +} + +describe('captureStructuredResponseFacts', () => { + it('commits labeled text facts as explicit intent nodes tied to the exchange response', () => { + const executor = new RecordingExecutor({ + status: 'success', + lsn: 7, + createdNodes: { + goal: { id: 11, code: 'G1' }, + context: { id: 12, code: 'CTX1' }, + constraint: { id: 13, code: 'CON1' }, + criterion: { id: 14, code: 'CR1' }, + }, + edges: [], + }); + + const outcome = captureStructuredResponseFacts({ + specId: 42, + exchangeId: 'grounding-text-2', + answer: { + text: [ + 'Goal: Help local teams coordinate product specifications.', + 'Context: Designers will review the graph in a web UI.', + 'Constraint: Keep graph writes synchronous for the POC.', + 'Criterion: Observers can see the selected spec update.', + ].join('\n'), + }, + commandExecutor: executor as unknown as CommandExecutor, + }); + + expect(outcome).toEqual({ + status: 'captured', + lsn: 7, + nodeCount: 4, + createdNodes: { + goal: { id: 11, code: 'G1' }, + context: { id: 12, code: 'CTX1' }, + constraint: { id: 13, code: 'CON1' }, + criterion: { id: 14, code: 'CR1' }, + }, + }); + expect(executor.calls).toEqual([ + { + specId: 42, + basis: 'explicit', + nodes: [ + { + ref: 'goal', + plane: 'intent', + kind: 'goal', + title: 'Help local teams coordinate product specifications.', + source: 'structured_exchange_response:grounding-text-2', + }, + { + ref: 'context', + plane: 'intent', + kind: 'context', + title: 'Designers will review the graph in a web UI.', + source: 'structured_exchange_response:grounding-text-2', + }, + { + ref: 'constraint', + plane: 'intent', + kind: 'constraint', + title: 'Keep graph writes synchronous for the POC.', + source: 'structured_exchange_response:grounding-text-2', + }, + { + ref: 'criterion', + plane: 'intent', + kind: 'criterion', + title: 'Observers can see the selected spec update.', + source: 'structured_exchange_response:grounding-text-2', + }, + ], + edges: [], + }, + ]); + }); + + it('returns no_capture for ambiguous or implication-only prose without invoking CommandExecutor', () => { + const executor = new RecordingExecutor({ status: 'success', lsn: 1, createdNodes: {}, edges: [] }); + + const outcome = captureStructuredResponseFacts({ + specId: 42, + exchangeId: 'grounding-text-2', + answer: { text: 'We probably need something that helps people decide what matters later.' }, + commandExecutor: executor as unknown as CommandExecutor, + }); + + expect(outcome).toEqual({ + status: 'no_capture', + reason: 'No directly labeled high-confidence graph facts found.', + }); + expect(executor.calls).toEqual([]); + }); + + it('passes structural_illegal diagnostics from CommandExecutor through unchanged', () => { + const diagnostic = { field: 'nodes[0].kind', message: 'kind is not valid for plane' }; + const executor = new RecordingExecutor({ status: 'structural_illegal', diagnostics: [diagnostic] }); + + const outcome = captureStructuredResponseFacts({ + specId: 42, + exchangeId: 'grounding-text-2', + answer: { text: 'Goal: Name the invalid fact.' }, + commandExecutor: executor as unknown as CommandExecutor, + }); + + expect(outcome).toEqual({ status: 'structural_illegal', diagnostics: [diagnostic] }); + }); + + it('does not capture non-text structured exchange answers', () => { + const executor = new RecordingExecutor({ status: 'success', lsn: 1, createdNodes: {}, edges: [] }); + + const outcome = captureStructuredResponseFacts({ + specId: 42, + exchangeId: 'grounding-choice-1', + answer: { optionId: 'new-from-scratch' }, + commandExecutor: executor as unknown as CommandExecutor, + }); + + expect(outcome.status).toBe('no_capture'); + expect(executor.calls).toEqual([]); + }); +}); diff --git a/src/graph/capture/structured-response.ts b/src/graph/capture/structured-response.ts new file mode 100644 index 000000000..fbb464b73 --- /dev/null +++ b/src/graph/capture/structured-response.ts @@ -0,0 +1,107 @@ +import type { CommandExecutor } from '../command-executor.js'; +import type { + CommitGraphInput, + CreatedGraphNodes, + Diagnostic, +} from '../command-executor/commit-graph-types.js'; +import type { NodePlane } from '../schema/nodes.js'; + +const CAPTURE_SOURCE_PREFIX = 'structured_exchange_response:'; +const LABELED_FACTS: Record = { + goal: { kind: 'goal', ref: 'goal' }, + context: { kind: 'context', ref: 'context' }, + constraint: { kind: 'constraint', ref: 'constraint' }, + criterion: { kind: 'criterion', ref: 'criterion' }, +}; + +const INTENT_PLANE: NodePlane = 'intent'; + +export type StructuredResponseCaptureOutcome = + | { + readonly status: 'captured'; + readonly lsn: number; + readonly nodeCount: number; + readonly createdNodes: CreatedGraphNodes; + } + | { + readonly status: 'no_capture'; + readonly reason: string; + } + | { + readonly status: 'structural_illegal'; + readonly diagnostics: readonly Diagnostic[]; + }; + +export interface StructuredResponseCaptureInput { + readonly specId: number; + readonly exchangeId: string; + readonly answer: unknown; + readonly commandExecutor: CommandExecutor; +} + +export function captureStructuredResponseFacts( + input: StructuredResponseCaptureInput, +): StructuredResponseCaptureOutcome { + const text = textAnswer(input.answer); + if (text === undefined) { + return { status: 'no_capture', reason: 'Only text structured exchange answers are capture candidates.' }; + } + + const nodes = extractLabeledIntentNodes(text, input.exchangeId); + if (nodes.length === 0) { + return { + status: 'no_capture', + reason: 'No directly labeled high-confidence graph facts found.', + }; + } + + const command: CommitGraphInput = { + specId: input.specId, + basis: 'explicit', + nodes, + edges: [], + }; + const result = input.commandExecutor.commitGraph(command); + if (result.status === 'structural_illegal') return result; + + return { + status: 'captured', + lsn: result.lsn, + nodeCount: Object.keys(result.createdNodes).length, + createdNodes: result.createdNodes, + }; +} + +function textAnswer(answer: unknown): string | undefined { + if (typeof answer !== 'object' || answer === null) return undefined; + const value = (answer as { readonly text?: unknown }).text; + return typeof value === 'string' ? value : undefined; +} + +type CapturedNode = CommitGraphInput['nodes'][number]; + +function extractLabeledIntentNodes(text: string, exchangeId: string): CapturedNode[] { + const captured: CapturedNode[] = []; + const seenRefs = new Set(); + + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.trim().replace(/^[-*]\s+/, ''); + const match = /^(goal|context|constraint|criterion):\s*(.+)$/i.exec(line); + if (!match) continue; + + const label = match[1]!.toLowerCase(); + const title = match[2]!.trim(); + const fact = LABELED_FACTS[label]; + if (!fact || title.length === 0 || seenRefs.has(fact.ref)) continue; + seenRefs.add(fact.ref); + captured.push({ + ref: fact.ref, + plane: INTENT_PLANE, + kind: fact.kind, + title, + source: `${CAPTURE_SOURCE_PREFIX}${exchangeId}`, + }); + } + + return captured; +} diff --git a/src/graph/seed-fixtures.test.ts b/src/graph/seed-fixtures.test.ts index f59dd1c9c..61682a3a4 100644 --- a/src/graph/seed-fixtures.test.ts +++ b/src/graph/seed-fixtures.test.ts @@ -18,8 +18,8 @@ import { seedFixture, type SeedFixture } from './seed-fixtures.js'; const HERE = dirname(fileURLToPath(import.meta.url)); -function loadFixture(slug: string): SeedFixture { - const path = resolve(HERE, `../../.fixtures/seeds/bilal-port/${slug}.json`); +function loadFixture(slug: string, set = 'bilal-port'): SeedFixture { + const path = resolve(HERE, `../../.fixtures/seeds/${set}/${slug}.json`); return JSON.parse(readFileSync(path, 'utf8')) as SeedFixture; } @@ -61,6 +61,25 @@ describe('seedFixture', () => { expect(ops).toEqual(['create_spec', 'commit_graph']); }); + it('loads the macro-view grounded-intent variant as explicit intent-only seed truth', () => { + const db: BrunchDb = createDb(':memory:'); + const executor = new CommandExecutor(db); + const fixture = loadFixture('macro-view-grounded-intent', 'bilal-port-variants'); + + expect(fixture.nodes.length).toBeGreaterThan(0); + expect(fixture.nodes.every((node) => node.plane === 'intent')).toBe(true); + expect(fixture.nodes.every((node) => node.basis === 'explicit')).toBe(true); + expect(fixture.edges.every((edge) => edge.basis === 'explicit')).toBe(true); + + const result = seedFixture(executor, fixture); + + const nodeRows = db.select().from(nodes).where(eq(nodes.spec_id, result.specId)).all(); + const edgeRows = db.select().from(edges).where(eq(edges.spec_id, result.specId)).all(); + expect(nodeRows).toHaveLength(fixture.nodes.length); + expect(edgeRows).toHaveLength(fixture.edges.length); + expect(nodeRows.every((row) => row.plane === 'intent' && row.basis === 'explicit')).toBe(true); + }); + it('rejects fixtures carrying a non-explicit basis', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); diff --git a/src/probes/capture-response-to-graph-proof.test.ts b/src/probes/capture-response-to-graph-proof.test.ts new file mode 100644 index 000000000..c3a9c0171 --- /dev/null +++ b/src/probes/capture-response-to-graph-proof.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { runCaptureResponseToGraphProof } from './capture-response-to-graph-proof.js'; + +describe('capture response to graph proof', () => { + it('proves public RPC activation, trigger, submit, and overview path without graph/capture imports', async () => { + const report = await runCaptureResponseToGraphProof({ runId: 'unit-proof' }); + + expect(report).toMatchObject({ + schemaVersion: 1, + probeId: 'capture-response-to-graph', + runId: 'unit-proof', + specId: expect.any(Number), + sessionId: expect.any(String), + exchangeId: 'deterministic-grounding-text-2', + capture: { status: 'captured', nodeCount: 4, lsn: expect.any(Number) }, + graph: { + nodeCount: 4, + codes: ['G1', 'CTX1', 'CON1', 'CR1'], + lsn: expect.any(Number), + }, + updates: expect.arrayContaining([ + { topic: 'graph.overview', specId: expect.any(Number), lsn: expect.any(Number) }, + { topic: 'graph.nodeNeighborhood', specId: expect.any(Number), lsn: expect.any(Number) }, + ]), + friction: [], + }); + expect(report.graph.lsn).toBeGreaterThanOrEqual(report.capture.lsn); + }); + + it('writes transcript, capture outcome, graph evidence, lsn, and friction artifacts', async () => { + const fixtureRoot = await mkdtemp(join(tmpdir(), 'brunch-capture-fixtures-')); + + const report = await runCaptureResponseToGraphProof({ fixtureRoot, runId: 'artifact-test' }); + + expect(report.artifacts).toEqual({ + runDir: join(fixtureRoot, 'runs', 'capture-response-to-graph', 'artifact-test'), + sessionJsonl: join(fixtureRoot, 'runs', 'capture-response-to-graph', 'artifact-test', 'session.jsonl'), + transcriptMarkdown: join( + fixtureRoot, + 'runs', + 'capture-response-to-graph', + 'artifact-test', + 'transcript.md', + ), + reportJson: join(fixtureRoot, 'runs', 'capture-response-to-graph', 'artifact-test', 'report.json'), + }); + if (!report.artifacts) throw new Error('expected artifacts'); + + const sessionJsonl = await readFile(report.artifacts.sessionJsonl, 'utf8'); + const transcript = await readFile(report.artifacts.transcriptMarkdown, 'utf8'); + const persistedReport = JSON.parse(await readFile(report.artifacts.reportJson, 'utf8')) as typeof report; + + expect(sessionJsonl).toContain('Goal: Help product teams turn elicitation answers into graph truth.'); + expect(transcript).toContain('Tool result: request_answer'); + expect(persistedReport.capture).toEqual(report.capture); + expect(persistedReport.graph.codes).toEqual(['G1', 'CTX1', 'CON1', 'CR1']); + expect(persistedReport.friction).toEqual([]); + }); +}); diff --git a/src/probes/capture-response-to-graph-proof.ts b/src/probes/capture-response-to-graph-proof.ts new file mode 100644 index 000000000..5c8152fcb --- /dev/null +++ b/src/probes/capture-response-to-graph-proof.ts @@ -0,0 +1,220 @@ +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { formatGraphNodeCode } from '../graph/schema/nodes.js'; +import type { GraphOverview } from '../graph/snapshot.js'; +import { createRpcHandlers } from '../rpc/handlers.js'; +import { createProductUpdatePublisher, type ProductUpdate } from '../rpc/product-updates.js'; +import { renderSessionTranscript } from '../session/session-transcript.js'; +import { createWorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; + +interface JsonRpcSuccess { + readonly jsonrpc: '2.0'; + readonly id: number; + readonly result: T; +} + +interface PendingExchangeResult { + readonly status: 'pending'; + readonly exchange: { + readonly exchangeId: string; + readonly mode: 'text' | 'single-select' | 'multi-select'; + }; +} + +interface SubmitResponseResult { + readonly status: 'accepted'; + readonly exchangeId: string; + readonly capture: CaptureOutcome; +} + +interface CaptureOutcome { + readonly status: 'captured'; + readonly lsn: number; + readonly nodeCount: number; + readonly createdNodes: Record; +} + +export interface CaptureResponseToGraphProofArtifacts { + readonly runDir: string; + readonly sessionJsonl: string; + readonly transcriptMarkdown: string; + readonly reportJson: string; +} + +export interface CaptureResponseToGraphProofOptions { + readonly fixtureRoot?: string; + readonly runId?: string; +} + +export interface CaptureResponseToGraphProofReport { + readonly schemaVersion: 1; + readonly probeId: 'capture-response-to-graph'; + readonly runId: string; + readonly generatedAt: string; + readonly mission: string; + readonly evaluationFocus: string; + readonly cwd: string; + readonly specId: number; + readonly sessionId: string; + readonly exchangeId: string; + readonly capture: CaptureOutcome; + readonly graph: { + readonly nodeCount: number; + readonly edgeCount: number; + readonly lsn: number; + readonly codes: readonly string[]; + readonly titles: readonly string[]; + }; + readonly updates: readonly ProductUpdate[]; + readonly friction: readonly string[]; + readonly artifacts?: CaptureResponseToGraphProofArtifacts; +} + +const CAPTURE_TEXT = [ + 'Goal: Help product teams turn elicitation answers into graph truth.', + 'Context: Designers will observe the graph from a web UI.', + 'Constraint: Use the selected session binding, not workspace defaults.', + 'Criterion: The selected spec overview shows projected graph codes.', +].join('\n'); + +export async function runCaptureResponseToGraphProof( + options: CaptureResponseToGraphProofOptions = {}, +): Promise { + const runId = + options.runId ?? + new Date() + .toISOString() + .replaceAll(':', '-') + .replace(/\.\d{3}Z$/, 'Z'); + const generatedAt = new Date().toISOString(); + const cwd = await mkdtemp(join(tmpdir(), 'brunch-capture-response-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const productUpdates = createProductUpdatePublisher(); + const updates: ProductUpdate[] = []; + productUpdates.subscribe((batch) => updates.push(...batch)); + const handlers = createRpcHandlers({ coordinator, cwd, productUpdates }); + const friction: string[] = []; + + await handlers.handle({ + jsonrpc: '2.0', + id: 1, + method: 'workspace.activate', + params: { decision: { action: 'newSpec', title: 'Capture response proof spec' } }, + }); + const workspace = await coordinator.openDefaultWorkspace(); + if (workspace.status !== 'ready') + throw new Error('workspace.activate(newSpec) did not create a ready workspace'); + + const first = success( + await handlers.handle({ jsonrpc: '2.0', id: 2, method: 'session.triggerExchange' }), + ); + await handlers.handle({ + jsonrpc: '2.0', + id: 3, + method: 'session.submitExchangeResponse', + params: { exchangeId: first.exchange.exchangeId, answer: { optionId: 'new-from-scratch' } }, + }); + + const textExchange = success( + await handlers.handle({ jsonrpc: '2.0', id: 4, method: 'session.triggerExchange' }), + ); + if (textExchange.exchange.mode !== 'text') { + throw new Error(`Expected deterministic text exchange, got ${textExchange.exchange.mode}`); + } + + const submitted = success( + await handlers.handle({ + jsonrpc: '2.0', + id: 5, + method: 'session.submitExchangeResponse', + params: { + exchangeId: textExchange.exchange.exchangeId, + answer: { text: CAPTURE_TEXT }, + }, + }), + ); + if (submitted.capture.status !== 'captured') { + throw new Error(`Expected capture success, got ${JSON.stringify(submitted.capture)}`); + } + + const overview = success( + await handlers.handle({ + jsonrpc: '2.0', + id: 6, + method: 'graph.overview', + params: { specId: workspace.spec.id }, + }), + ); + if (overview.nodeCount !== submitted.capture.nodeCount) { + friction.push( + `Overview node count ${overview.nodeCount} did not match capture count ${submitted.capture.nodeCount}.`, + ); + } + + const orderedNodes = [...overview.nodes].sort( + (left, right) => captureKindOrder(left.kind) - captureKindOrder(right.kind), + ); + + const graph = { + nodeCount: overview.nodeCount, + edgeCount: overview.edgeCount, + lsn: overview.lsn, + codes: orderedNodes.map((node) => formatGraphNodeCode(node.kind, node.kindOrdinal)), + titles: orderedNodes.map((node) => node.title), + }; + + const sessionJsonl = await readFile(workspace.session.file, 'utf8'); + const transcriptMarkdown = renderSessionTranscript(sessionJsonl, { title: 'session.jsonl' }); + + const reportWithoutArtifacts = { + schemaVersion: 1 as const, + probeId: 'capture-response-to-graph' as const, + runId, + generatedAt, + mission: + 'Drive a transcript-native structured text answer through public RPC capture into selected-spec graph truth.', + evaluationFocus: + 'Public RPC activation/trigger/submit/overview path, explicit-basis capture outcome, graph counts/codes, LSN, transcript evidence, and observer invalidations.', + cwd, + specId: workspace.spec.id, + sessionId: workspace.session.id, + exchangeId: textExchange.exchange.exchangeId, + capture: submitted.capture, + graph, + updates, + friction, + }; + + if (options.fixtureRoot === undefined) return reportWithoutArtifacts; + + const runDir = join(options.fixtureRoot, 'runs', 'capture-response-to-graph', runId); + await mkdir(runDir, { recursive: true }); + const artifacts = { + runDir, + sessionJsonl: join(runDir, 'session.jsonl'), + transcriptMarkdown: join(runDir, 'transcript.md'), + reportJson: join(runDir, 'report.json'), + }; + const report = { ...reportWithoutArtifacts, artifacts }; + await writeFile(artifacts.sessionJsonl, sessionJsonl); + await writeFile(artifacts.transcriptMarkdown, transcriptMarkdown); + await writeFile(artifacts.reportJson, `${JSON.stringify(report, null, 2)}\n`); + return report; +} + +function success(response: unknown): T { + if (typeof response === 'object' && response !== null && 'result' in response) { + return (response as JsonRpcSuccess).result; + } + throw new Error(`Expected JSON-RPC success response: ${JSON.stringify(response)}`); +} + +function captureKindOrder(kind: string): number { + if (kind === 'goal') return 0; + if (kind === 'context') return 1; + if (kind === 'constraint') return 2; + if (kind === 'criterion') return 3; + return 4; +} diff --git a/src/probes/fixture-curation-loop.test.ts b/src/probes/fixture-curation-loop.test.ts new file mode 100644 index 000000000..712f186ca --- /dev/null +++ b/src/probes/fixture-curation-loop.test.ts @@ -0,0 +1,200 @@ +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import type { GraphOverview } from '../graph/snapshot.js'; +import { + summarizeFixtureCurationRun, + writeFixtureCurationArtifacts, + type FixtureCurationReport, +} from './fixture-curation-loop.js'; + +function toolResultEntry(toolName: string, details: unknown): string { + return JSON.stringify({ + type: 'message', + message: { + role: 'toolResult', + toolName, + content: [{ type: 'text', text: JSON.stringify(details) }], + details, + }, + }); +} + +const mixedBasisOverview: GraphOverview = { + nodes: [ + { + id: 1, + specId: 7, + plane: 'intent', + kind: 'goal', + kindOrdinal: 1, + title: 'Base launch goal', + basis: 'explicit', + createdAtLsn: 2, + updatedAtLsn: 2, + }, + { + id: 2, + specId: 7, + plane: 'intent', + kind: 'requirement', + kindOrdinal: 1, + title: 'Rollback path is named', + basis: 'implicit', + createdAtLsn: 3, + updatedAtLsn: 3, + }, + ], + edges: [ + { + id: 1, + specId: 7, + sourceId: 2, + targetId: 1, + category: 'support', + stance: 'for', + basis: 'implicit', + createdAtLsn: 3, + updatedAtLsn: 3, + }, + ], + nodeCount: 2, + edgeCount: 1, + lsn: 3, +}; + +describe('fixture curation loop report', () => { + it('requires real commit_graph transcript evidence and implicit graph readback', () => { + const report = summarizeFixtureCurationRun({ + runId: 'fixture-curation-test', + generatedAt: '2026-06-05T00:00:00.000Z', + cwd: '/tmp/brunch-fixture-curation-test', + seedSlug: 'macro-view-grounded-intent', + selectedBaseProfile: 'grounded-intent', + specId: 7, + sessionId: 'session-1', + prompt: 'Please curate the graph.', + runtimeState: { + operationalMode: 'elicit', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'commit-converge', + }, + model: 'test-model', + sessionText: [ + toolResultEntry('read_graph', { status: 'success' }), + toolResultEntry('commit_graph', { + status: 'success', + lsn: 3, + createdNodes: { rollback: { id: 2 } }, + edges: [1], + }), + ].join('\n'), + overview: mixedBasisOverview, + }); + + expect(report.success).toBe(true); + expect(report.commitGraphAttemptCount).toBe(1); + expect(report.commitGraphAttempts[0]).toMatchObject({ status: 'success', lsn: 3 }); + expect(report.createdNodes).toEqual([ + { + id: 2, + code: 'R1', + plane: 'intent', + kind: 'requirement', + title: 'Rollback path is named', + basis: 'implicit', + }, + ]); + expect(report.finalGraph).toMatchObject({ + nodeCount: 2, + edgeCount: 1, + explicitNodeCount: 1, + implicitNodeCount: 1, + explicitEdgeCount: 0, + implicitEdgeCount: 1, + }); + expect(report.friction).toEqual([]); + }); + + it('fails closed when a graph has only explicit base truth', () => { + const report = summarizeFixtureCurationRun({ + runId: 'fixture-curation-test', + generatedAt: '2026-06-05T00:00:00.000Z', + cwd: '/tmp/brunch-fixture-curation-test', + seedSlug: 'macro-view-grounded-intent', + selectedBaseProfile: 'grounded-intent', + specId: 7, + sessionId: 'session-1', + prompt: 'Please curate the graph.', + runtimeState: { + operationalMode: 'elicit', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'commit-converge', + }, + sessionText: toolResultEntry('commit_graph', { + status: 'success', + lsn: 2, + createdNodes: {}, + edges: [], + }), + overview: { + ...mixedBasisOverview, + nodes: [mixedBasisOverview.nodes[0]!], + edges: [], + nodeCount: 1, + edgeCount: 0, + }, + }); + + expect(report.success).toBe(false); + expect(report.createdNodes).toEqual([]); + expect(report.friction).toContain('No implicit graph nodes were present in graph readback.'); + }); + + it('writes session, transcript, report, and graph snapshot artifacts', async () => { + const fixtureRoot = await mkdtemp(join(tmpdir(), 'brunch-fixture-curation-artifacts-')); + const report: FixtureCurationReport = summarizeFixtureCurationRun({ + runId: 'fixture-curation-test', + generatedAt: '2026-06-05T00:00:00.000Z', + cwd: fixtureRoot, + seedSlug: 'macro-view-grounded-intent', + selectedBaseProfile: 'grounded-intent', + specId: 7, + sessionId: 'session-1', + prompt: 'Please curate the graph.', + runtimeState: { + operationalMode: 'elicit', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'commit-converge', + }, + sessionText: toolResultEntry('commit_graph', { + status: 'success', + lsn: 3, + createdNodes: { node: { id: 2 } }, + }), + overview: mixedBasisOverview, + }); + + const artifacts = await writeFixtureCurationArtifacts({ + fixtureRoot, + runId: 'fixture-curation-test', + sessionText: toolResultEntry('commit_graph', { status: 'success' }), + report, + graphSnapshot: mixedBasisOverview, + }); + + expect(artifacts.runDir).toBe(join(fixtureRoot, 'runs', 'fixture-curation', 'fixture-curation-test')); + await expect(readFile(artifacts.sessionJsonl, 'utf8')).resolves.toContain('"toolName":"commit_graph"'); + await expect(readFile(artifacts.transcriptMarkdown, 'utf8')).resolves.toContain('## Raw session JSONL'); + await expect(readFile(artifacts.reportJson, 'utf8')).resolves.toContain( + '"seedSlug": "macro-view-grounded-intent"', + ); + await expect(readFile(artifacts.graphSnapshotJson, 'utf8')).resolves.toContain('"basis": "implicit"'); + }); +}); diff --git a/src/probes/fixture-curation-loop.ts b/src/probes/fixture-curation-loop.ts new file mode 100644 index 000000000..fc23c77b6 --- /dev/null +++ b/src/probes/fixture-curation-loop.ts @@ -0,0 +1,472 @@ +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import { getAgentDir } from '@earendil-works/pi-coding-agent'; + +import { appendBrunchAgentRuntimeSwitch, type BrunchAgentState } from '../.pi/extensions/operational-mode.js'; +import { createBrunchAgentSessionRuntimeFactory } from '../brunch-tui.js'; +import { + formatGraphNodeCode, + openWorkspaceGraphRuntime, + type CommitGraphSuccess, + type Diagnostic, + type GraphNode, + type GraphOverview, + type StructuralIllegal, +} from '../graph/index.js'; +import { seedFixture, type SeedFixture } from '../graph/seed-fixtures.js'; +import { renderSessionTranscript } from '../session/session-transcript.js'; +import { createWorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; + +const PROBE_ID = 'fixture-curation' as const; +const DEFAULT_SEED_SET = 'bilal-port-variants'; +const DEFAULT_SEED_SLUG = 'macro-view-grounded-intent'; + +export type FixtureCurationCommitStatus = + | CommitGraphSuccess['status'] + | StructuralIllegal['status'] + | 'needs_human' + | 'policy_blocked' + | 'version_conflict' + | 'unknown'; + +export interface FixtureCurationRuntimeStateReport { + readonly operationalMode: 'elicit'; + readonly agentStrategy: 'propose-graph'; + readonly agentLens: 'intent'; + readonly agentGoal: 'commit-converge'; +} + +export interface FixtureCurationRunOptions { + readonly cwd?: string; + readonly fixtureRoot?: string; + readonly seedSet?: string; + readonly seedSlug?: string; + readonly selectedBaseProfile?: string; + readonly runId?: string; + readonly prompt?: string; + readonly agentDir?: string; +} + +export interface FixtureCurationArtifacts { + readonly runDir: string; + readonly sessionJsonl: string; + readonly transcriptMarkdown: string; + readonly reportJson: string; + readonly graphSnapshotJson: string; +} + +export interface FixtureCurationCommitAttempt { + readonly index: number; + readonly status: FixtureCurationCommitStatus; + readonly lsn?: number; + readonly nodeRefs?: Record; + readonly edgeIds?: number[]; + readonly diagnostics?: Diagnostic[]; + readonly content?: string; +} + +export interface FixtureCurationCreatedNode { + readonly id: number; + readonly code: string; + readonly plane: GraphNode['plane']; + readonly kind: GraphNode['kind']; + readonly title: string; + readonly basis: 'implicit'; +} + +export interface FixtureCurationReport { + readonly schemaVersion: 1; + readonly probeId: typeof PROBE_ID; + readonly runId: string; + readonly generatedAt: string; + readonly seedSet: string; + readonly seedSlug: string; + readonly selectedBaseProfile: string; + readonly cwd: string; + readonly specId: number; + readonly sessionId: string; + readonly prompt: string; + readonly runtimeState: FixtureCurationRuntimeStateReport; + readonly model?: string; + readonly success: boolean; + readonly commitGraphAttemptCount: number; + readonly commitGraphAttempts: FixtureCurationCommitAttempt[]; + readonly createdNodes: FixtureCurationCreatedNode[]; + readonly finalGraph: { + readonly nodeCount: number; + readonly edgeCount: number; + readonly lsn: number; + readonly explicitNodeCount: number; + readonly implicitNodeCount: number; + readonly explicitEdgeCount: number; + readonly implicitEdgeCount: number; + }; + readonly friction: string[]; + readonly artifacts?: FixtureCurationArtifacts; +} + +export interface FixtureCurationSummaryInput { + readonly runId: string; + readonly generatedAt: string; + readonly cwd: string; + readonly seedSet?: string; + readonly seedSlug: string; + readonly selectedBaseProfile: string; + readonly specId: number; + readonly sessionId: string; + readonly prompt: string; + readonly runtimeState: FixtureCurationRuntimeStateReport; + readonly model?: string; + readonly sessionText: string; + readonly overview: GraphOverview; + readonly friction?: readonly string[]; +} + +export async function runFixtureCurationLoop( + options: FixtureCurationRunOptions = {}, +): Promise { + const cwd = resolve(options.cwd ?? (await mkdtemp(join(tmpdir(), 'brunch-fixture-curation-')))); + const fixtureRoot = resolve( + options.fixtureRoot ?? join(dirname(fileURLToPath(import.meta.url)), '../../.fixtures'), + ); + const seedSet = options.seedSet ?? DEFAULT_SEED_SET; + const seedSlug = options.seedSlug ?? DEFAULT_SEED_SLUG; + const selectedBaseProfile = options.selectedBaseProfile ?? 'grounded-intent'; + const runId = options.runId ?? defaultRunId(); + const prompt = options.prompt ?? defaultCurationPrompt(); + const generatedAt = new Date().toISOString(); + const fixture = await readSeedFixture(join(fixtureRoot, 'seeds', seedSet, `${seedSlug}.json`)); + const graph = await openWorkspaceGraphRuntime(cwd); + const seedResult = seedFixture(graph.commandExecutor, fixture); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + await coordinator.openDefaultWorkspace(); + await selectSpecForSetupSession(cwd, seedResult.specId); + const activated = await coordinator.activateWorkspace({ action: 'newSession', specId: seedResult.specId }); + if (activated.status !== 'ready') { + throw new Error(`fixture curation could not activate seeded spec: ${activated.status}`); + } + + const runtimeState: BrunchAgentState = { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'commit-converge', + }; + const runtimeStateReport: FixtureCurationRuntimeStateReport = { + operationalMode: 'elicit', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'commit-converge', + }; + appendBrunchAgentRuntimeSwitch(activated.session.manager, runtimeState, 'extension'); + const createRuntime = createBrunchAgentSessionRuntimeFactory({ workspace: activated, coordinator }); + const created = await createRuntime({ + cwd, + agentDir: options.agentDir ?? getAgentDir(), + sessionManager: activated.session.manager, + }); + const session = created.session; + const friction = created.diagnostics.map((diagnostic) => `${diagnostic.type}: ${diagnostic.message}`); + + try { + await session.sendUserMessage(prompt); + await session.agent.waitForIdle(); + const sessionText = await readFile(activated.session.file, 'utf8'); + const overview = graph.forSpec(seedResult.specId).getGraphOverview(); + let report = summarizeFixtureCurationRun({ + runId, + generatedAt, + cwd, + seedSet, + seedSlug, + selectedBaseProfile, + specId: seedResult.specId, + sessionId: activated.session.id, + prompt, + runtimeState: runtimeStateReport, + ...(session.model?.id !== undefined ? { model: session.model.id } : {}), + sessionText, + overview, + friction, + }); + + report = { + ...report, + artifacts: await writeFixtureCurationArtifacts({ + fixtureRoot, + runId, + sessionText, + report, + graphSnapshot: overview, + }), + }; + return report; + } finally { + session.dispose(); + } +} + +export function summarizeFixtureCurationRun(input: FixtureCurationSummaryInput): FixtureCurationReport { + const commitGraphAttempts = commitGraphAttemptsFromSession(input.sessionText); + const createdNodes = input.overview.nodes.flatMap((node): FixtureCurationCreatedNode[] => { + if (node.basis !== 'implicit') return []; + return [ + { + id: node.id, + code: formatGraphNodeCode(node.kind, node.kindOrdinal), + plane: node.plane, + kind: node.kind, + title: node.title, + basis: 'implicit', + }, + ]; + }); + const explicitNodeCount = input.overview.nodes.filter((node) => node.basis === 'explicit').length; + const implicitNodeCount = createdNodes.length; + const explicitEdgeCount = input.overview.edges.filter((edge) => edge.basis === 'explicit').length; + const implicitEdgeCount = input.overview.edges.filter((edge) => edge.basis === 'implicit').length; + const friction = [...(input.friction ?? [])]; + const hasSuccessfulCommit = commitGraphAttempts.some((attempt) => attempt.status === 'success'); + + if (commitGraphAttempts.length === 0) { + friction.push('No commit_graph tool result was recorded in the session transcript.'); + } + if (!hasSuccessfulCommit && commitGraphAttempts.length > 0) { + friction.push( + `No commit_graph attempt succeeded; final status was ${commitGraphAttempts.at(-1)!.status}.`, + ); + } + if (implicitNodeCount === 0) { + friction.push('No implicit graph nodes were present in graph readback.'); + } + + return { + schemaVersion: 1, + probeId: PROBE_ID, + runId: input.runId, + generatedAt: input.generatedAt, + seedSet: input.seedSet ?? DEFAULT_SEED_SET, + seedSlug: input.seedSlug, + selectedBaseProfile: input.selectedBaseProfile, + cwd: input.cwd, + specId: input.specId, + sessionId: input.sessionId, + prompt: input.prompt, + runtimeState: input.runtimeState, + ...(input.model !== undefined ? { model: input.model } : {}), + success: hasSuccessfulCommit && implicitNodeCount > 0, + commitGraphAttemptCount: commitGraphAttempts.length, + commitGraphAttempts, + createdNodes, + finalGraph: { + nodeCount: input.overview.nodeCount, + edgeCount: input.overview.edgeCount, + lsn: input.overview.lsn, + explicitNodeCount, + implicitNodeCount, + explicitEdgeCount, + implicitEdgeCount, + }, + friction, + }; +} + +export async function writeFixtureCurationArtifacts(options: { + readonly fixtureRoot: string; + readonly runId: string; + readonly sessionText: string; + readonly report: FixtureCurationReport; + readonly graphSnapshot: GraphOverview; +}): Promise { + const runDir = join(options.fixtureRoot, 'runs', PROBE_ID, options.runId); + const artifacts: FixtureCurationArtifacts = { + runDir, + sessionJsonl: join(runDir, 'session.jsonl'), + transcriptMarkdown: join(runDir, 'transcript.md'), + reportJson: join(runDir, 'report.json'), + graphSnapshotJson: join(runDir, 'graph-snapshot.json'), + }; + const report = { ...options.report, artifacts }; + + await mkdir(runDir, { recursive: true }); + await writeFile(artifacts.sessionJsonl, options.sessionText, 'utf8'); + await writeFile( + artifacts.transcriptMarkdown, + `${renderSessionTranscript(options.sessionText, { title: 'session.jsonl' })}\n\n## Raw session JSONL\n\n\`\`\`jsonl\n${options.sessionText.trimEnd()}\n\`\`\`\n`, + 'utf8', + ); + await writeFile(artifacts.reportJson, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + await writeFile(artifacts.graphSnapshotJson, `${JSON.stringify(options.graphSnapshot, null, 2)}\n`, 'utf8'); + + return artifacts; +} + +async function readSeedFixture(path: string): Promise { + return JSON.parse(await readFile(path, 'utf8')) as SeedFixture; +} + +function commitGraphAttemptsFromSession(sessionText: string): FixtureCurationCommitAttempt[] { + const attempts: FixtureCurationCommitAttempt[] = []; + for (const line of sessionText.split('\n')) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + const entry = parseJson(trimmed); + if (!isRecord(entry) || entry.type !== 'message') continue; + const message = entry.message; + if (!isRecord(message) || message.role !== 'toolResult' || message.toolName !== 'commit_graph') { + continue; + } + attempts.push(commitGraphAttemptFromMessage(attempts.length + 1, message)); + } + return attempts; +} + +function commitGraphAttemptFromMessage( + index: number, + message: Record, +): FixtureCurationCommitAttempt { + const details = isRecord(message.details) ? message.details : undefined; + return { + index, + status: commitGraphStatus(details?.status), + ...(typeof details?.lsn === 'number' ? { lsn: details.lsn } : {}), + ...(isCreatedNodeRecord(details?.createdNodes) + ? { nodeRefs: mapCreatedNodeIds(details.createdNodes) } + : {}), + ...(isNumberArray(details?.edges) ? { edgeIds: details.edges } : {}), + ...(isDiagnosticArray(details?.diagnostics) ? { diagnostics: details.diagnostics } : {}), + ...textContent(message.content), + }; +} + +function commitGraphStatus(value: unknown): FixtureCurationCommitStatus { + if ( + value === 'success' || + value === 'structural_illegal' || + value === 'needs_human' || + value === 'policy_blocked' || + value === 'version_conflict' + ) { + return value; + } + return 'unknown'; +} + +function defaultCurationPrompt(): string { + return `Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named "${DEFAULT_SEED_SLUG}". + +Use read_graph once in overview mode. Then use commit_graph exactly once to add a small intent-plane expansion that improves launch/usefulness of this existing spec without duplicating base nodes. Create one to three new intent-plane nodes, connect them legally to existing graph truth when possible, use basis implicit through the propose-graph tool path, and stop after a successful commit_graph result.`; +} + +function defaultRunId(): string { + return new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-'); +} + +function parseJson(value: string): unknown { + try { + return JSON.parse(value) as unknown; + } catch { + return undefined; + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isDiagnosticArray(value: unknown): value is Diagnostic[] { + return ( + Array.isArray(value) && + value.every( + (item) => isRecord(item) && typeof item.field === 'string' && typeof item.message === 'string', + ) + ); +} + +function isCreatedNodeRecord(value: unknown): value is Record { + return ( + isRecord(value) && Object.values(value).every((entry) => isRecord(entry) && typeof entry.id === 'number') + ); +} + +function mapCreatedNodeIds(value: Record): Record { + return Object.fromEntries(Object.entries(value).map(([ref, node]) => [ref, node.id])); +} + +function isNumberArray(value: unknown): value is number[] { + return Array.isArray(value) && value.every((entry): entry is number => typeof entry === 'number'); +} + +function textContent(value: unknown): { content?: string } { + if (typeof value === 'string') return { content: value }; + if (!Array.isArray(value)) return {}; + const text = value + .flatMap((item) => (isRecord(item) && typeof item.text === 'string' ? [item.text] : [])) + .join('\n'); + return text.length > 0 ? { content: text } : {}; +} + +function parseCliArgs(argv: readonly string[]): FixtureCurationRunOptions { + const options: Record = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg !== undefined && arg.startsWith('--')) { + options[arg.slice(2)] = requiredValue(argv, (index += 1), arg); + } + } + return { + ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), + ...(options['fixture-root'] !== undefined ? { fixtureRoot: options['fixture-root'] } : {}), + ...(options['seed-set'] !== undefined ? { seedSet: options['seed-set'] } : {}), + ...(options['seed-slug'] !== undefined ? { seedSlug: options['seed-slug'] } : {}), + ...(options['selected-base-profile'] !== undefined + ? { selectedBaseProfile: options['selected-base-profile'] } + : {}), + ...(options['run-id'] !== undefined ? { runId: options['run-id'] } : {}), + ...(options.prompt !== undefined ? { prompt: options.prompt } : {}), + ...(options['agent-dir'] !== undefined ? { agentDir: options['agent-dir'] } : {}), + }; +} + +async function selectSpecForSetupSession(cwd: string, specId: number): Promise { + const path = join(cwd, '.brunch', 'workspace.json'); + const state = JSON.parse(await readFile(path, 'utf8')) as Record; + await writeFile( + path, + `${JSON.stringify( + { + ...state, + current: { specId, sessionId: '' }, + }, + null, + 2, + )}\n`, + 'utf8', + ); +} + +function requiredValue(argv: readonly string[], index: number, flag: string): string { + const value = argv[index]; + if (value === undefined) { + throw new Error(`${flag} requires a value`); + } + return value; +} + +async function main(): Promise { + const report = await runFixtureCurationLoop(parseCliArgs(process.argv.slice(2))); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + process.exitCode = report.success ? 0 : 1; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/src/rpc/README.md b/src/rpc/README.md index ce8f57d9c..3183e334f 100644 --- a/src/rpc/README.md +++ b/src/rpc/README.md @@ -44,7 +44,7 @@ canonical stores: worldUpdate entries ``` -RPC handlers must not become a generic records API, REST read model, or canonical view store. Reads are named projections over the store that owns the fact. Mutations route through the owning product seam: session transcript operations through `session.*`, graph mutations through the agent/tool or `CommandExecutor` path that owns them. +RPC handlers must not become a generic records API, REST read model, or canonical view store. Reads are named projections over the store that owns the fact. Mutations route through the owning product seam: session transcript operations through `session.*`, synchronous high-confidence response capture through `session.submitExchangeResponse` → `graph/capture` → `CommandExecutor`, and other graph mutations through the agent/tool or `CommandExecutor` path that owns them. `dev.*` is the only exception family: methods in that namespace are explicitly gated local harnesses, absent from default discovery and absent from the read-only sidecar. ## Method registry @@ -53,14 +53,16 @@ Method discovery and dispatch come from the same registry. A method not present ```pseudo rpc/ ├── handlers.ts -│ ├── createRpcHandlers(...) -> full registry -│ ├── createReadOnlyRpcHandlers(...) -> read-only registry +│ ├── createRpcHandlers(...) -> default full registry +│ ├── createRpcHandlers({devRpc}) -> full registry plus gated dev.* harnesses +│ ├── createReadOnlyRpcHandlers(...) -> read-only registry, never dev.* │ └── rpc.discover -> discovery over active registry └── methods/ ├── registry.ts -> method definition + discovery shape ├── workspace.ts -> workspace.* handlers ├── session.ts -> session.* handlers ├── graph.ts -> graph.* handlers + ├── dev-graph.ts -> gated dev.graph.* fixture-curation harness └── schemas.ts -> shared protocol schemas ``` @@ -82,6 +84,15 @@ full RPC host: session.triggerExchange session.submitExchangeResponse +dev-enabled full RPC host only: + writes: + dev.graph.commitGraph + absent unless: + createRpcHandlers({devRpc: true}) or BRUNCH_DEV_RPC=1 in CLI rpc mode + still absent from: + default full RPC discovery + TUI-started web sidecar + TUI-started web sidecar: reads: rpc.discover @@ -160,8 +171,12 @@ session.submitExchangeResponse exchangeId answer: {text} | {optionId} | {optionIds} note? - result: accepted terminal response - effects: appends request_* toolResult response and publishes selected-session invalidations + result: accepted terminal response plus capture outcome + capture: + captured(lsn, nodeCount, createdNodes) + | no_capture(reason) + | structural_illegal(diagnostics) + effects: appends request_* toolResult response, publishes selected-session invalidations, and when captured publishes graph.overview / graph.nodeNeighborhood invalidations for the transcript-bound spec graph.overview access: read @@ -179,6 +194,19 @@ graph.nodeNeighborhood params: {specId, nodeId, hops?} result: success(anchor, neighbors, edges) | not_found source: SQLite graph reader for the explicit spec + +dev.graph.commitGraph + access: write + params: + specId + basis: explicit | implicit + nodes: [{ref, plane, kind, title, body?, source?, detail?}] + edges: [{category, source, target, stance?, rationale?}] + source/target: batch ref | {existingCode} + result: success(lsn, createdNodes, edges) | structural_illegal(diagnostics) + effects: commits atomically through CommandExecutor and publishes graph projection invalidations + gate: explicit local harness only; absent from default public RPC and read-only sidecars + caveat: fixture curation helper, not evidence that propose-graph's real agent commit_graph tool path works ``` ## Product update notifications @@ -227,7 +255,7 @@ query key families: | `session.exchanges` | `sessionExchangesQueryOptions(rpc, target)` | target; no current web history panel | `session.exchanges` | | `session.runtimeState` | `sessionRuntimeStateQueryOptions(rpc, target)` | implemented query option; not yet route-rendered | `session.runtimeState` | | `session.triggerExchange` | `triggerExchangeMutationOptions(rpc)` | target full-host mutation; sidecar rejects | invalidates pending/exchanges/runtime state | -| `session.submitExchangeResponse` | `submitExchangeResponseMutationOptions(rpc)` | target full-host mutation; sidecar rejects | invalidates pending/exchanges/runtime state; graph updates arrive after agent commit | +| `session.submitExchangeResponse` | `submitExchangeResponseMutationOptions(rpc)` | target full-host mutation; sidecar rejects | invalidates pending/exchanges/runtime state; captured text answers additionally invalidate `graph.overview(specId)` / `graph.nodeNeighborhood(specId)` | | `graph.overview` | `graphOverviewQueryOptions(rpc, specId)` | implemented; spec route loader primes it | exact `graph.overview(specId)` when `specId` is present | | `graph.nodeNeighborhood` | `graphNodeNeighborhoodQueryOptions(rpc, specId, nodeId, hops?)` | implemented query option; graph panel selection not yet wired | exact/prefix neighborhood invalidation when `nodeId` is present; broad topic fallback otherwise | @@ -271,8 +299,18 @@ capture_* toolResult (future) transcript evidence possible semantic candidates no graph mutation -``` +synchronous response capture (current POC tracer) + request_answer text with direct labels: + Goal: ... + Context: ... + Constraint: ... + Criterion: ... + -> graph/capture translator + -> CommandExecutor.commitGraph({basis: explicit}) + -> selected-spec graph truth + +``` Payload facets such as establishment offers, elicitor intent hints, and review/proposal material belong inside structured exchange payloads when they are part of an exchange. They are not separate public RPC entities. ## Web UI rules diff --git a/src/rpc/handlers.test.ts b/src/rpc/handlers.test.ts index 64eb97eae..ef5259a26 100644 --- a/src/rpc/handlers.test.ts +++ b/src/rpc/handlers.test.ts @@ -22,7 +22,7 @@ import type { SpecSessionActivationCoordinator, SpecSessionActivationDecision, } from '../session/workspace-session-coordinator.js'; -import { createRpcHandlers, runJsonRpcLineServer } from './handlers.js'; +import { createReadOnlyRpcHandlers, createRpcHandlers, runJsonRpcLineServer } from './handlers.js'; import { createProductUpdatePublisher } from './product-updates.js'; function coordinator( @@ -440,6 +440,7 @@ describe('JSON-RPC handlers', () => { methods: Array<{ method: string; paramsSchema: TSchema & { properties?: Record }; + resultSchema: TSchema & { properties?: Record }; examples: Array<{ params?: unknown }>; }>; } @@ -447,6 +448,7 @@ describe('JSON-RPC handlers', () => { const exchanges = methods.find((entry) => entry.method === 'session.exchanges'); const pending = methods.find((entry) => entry.method === 'session.pendingExchange'); const runtimeState = methods.find((entry) => entry.method === 'session.runtimeState'); + const submit = methods.find((entry) => entry.method === 'session.submitExchangeResponse'); for (const entry of [exchanges, pending]) { if (!entry) throw new Error('expected discovered selected-session projection method'); @@ -471,6 +473,11 @@ describe('JSON-RPC handlers', () => { for (const example of runtimeState.examples.filter((example) => example.params !== undefined)) { expect(Value.Check(runtimeState.paramsSchema, example.params)).toBe(true); } + + if (!submit) throw new Error('expected discovered session.submitExchangeResponse method'); + expect(JSON.stringify(submit.resultSchema.properties?.capture)).toContain('captured'); + expect(JSON.stringify(submit.resultSchema.properties?.capture)).toContain('no_capture'); + expect(JSON.stringify(submit.resultSchema.properties?.capture)).toContain('structural_illegal'); }); it('serves discovery examples that are valid JSON-RPC requests for advertised methods', async () => { @@ -1283,6 +1290,132 @@ describe('JSON-RPC handlers', () => { expect(sessionText).not.toContain('brunch.elicitation_response'); }); + it('captures explicit labeled text answers into the transcript-bound spec graph and publishes graph invalidations', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-response-capture-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); + const workspace = await coordinatorInstance.createSetupSession({ + specTitle: 'Transcript-bound spec', + }); + const graph = await openWorkspaceGraphRuntime(cwd); + const wrongSpec = graph.commandExecutor.createSpec({ name: 'Wrong default spec', slug: 'wrong-default' }); + if (wrongSpec.status !== 'success') throw new Error('failed to create wrong-spec fixture'); + const productUpdates = createProductUpdatePublisher(); + const updates: unknown[] = []; + productUpdates.subscribe((batch) => updates.push(...batch)); + const handlers = createRpcHandlers({ + coordinator: coordinator({ + ...workspace, + spec: { id: wrongSpec.specId, title: 'Wrong default spec' }, + }), + cwd, + productUpdates, + }); + + const first = await handlers.handle({ + jsonrpc: '2.0', + id: 270, + method: 'session.triggerExchange', + }); + const firstExchangeId = ( + first as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; + await handlers.handle({ + jsonrpc: '2.0', + id: 271, + method: 'session.submitExchangeResponse', + params: { exchangeId: firstExchangeId, answer: { optionId: 'new-from-scratch' } }, + }); + await handlers.handle({ + jsonrpc: '2.0', + id: 272, + method: 'session.triggerExchange', + }); + + const response = await handlers.handle({ + jsonrpc: '2.0', + id: 273, + method: 'session.submitExchangeResponse', + params: { + exchangeId: 'deterministic-grounding-text-2', + answer: { + text: [ + 'Goal: Help product teams turn elicitation answers into graph truth.', + 'Context: Designers will observe the graph from a web UI.', + 'Constraint: Use the selected session binding, not workspace defaults.', + 'Criterion: The selected spec overview shows projected graph codes.', + ].join('\n'), + }, + }, + }); + + expect(response).toMatchObject({ + jsonrpc: '2.0', + id: 273, + result: { + status: 'accepted', + capture: { + status: 'captured', + lsn: expect.any(Number), + nodeCount: 4, + createdNodes: { + goal: { code: 'G1' }, + context: { code: 'CTX1' }, + constraint: { code: 'CON1' }, + criterion: { code: 'CR1' }, + }, + }, + }, + }); + if (!('result' in response)) throw new Error('expected capture response'); + const lsn = (response.result as { capture: { lsn: number } }).capture.lsn; + expect(updates).toContainEqual({ topic: 'graph.overview', specId: workspace.spec.id, lsn }); + expect(updates).toContainEqual({ + topic: 'graph.nodeNeighborhood', + specId: workspace.spec.id, + lsn, + }); + + const overview = await handlers.handle({ + jsonrpc: '2.0', + id: 274, + method: 'graph.overview', + params: { specId: workspace.spec.id }, + }); + expect(overview).toMatchObject({ + result: { + nodeCount: 4, + nodes: expect.arrayContaining([ + expect.objectContaining({ + kind: 'goal', + kindOrdinal: 1, + basis: 'explicit', + source: 'structured_exchange_response:deterministic-grounding-text-2', + }), + expect.objectContaining({ + kind: 'criterion', + kindOrdinal: 1, + basis: 'explicit', + }), + ]), + }, + }); + + await expect( + handlers.handle({ + jsonrpc: '2.0', + id: 275, + method: 'graph.overview', + params: { specId: wrongSpec.specId }, + }), + ).resolves.toMatchObject({ + result: { + nodeCount: 0, + }, + }); + }); + it('rejects mismatched elicitation response ids without appending transcript entries', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-respond-bad-id-')); const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); @@ -1999,6 +2132,250 @@ describe('JSON-RPC handlers', () => { }); }); + it('keeps dev graph commit RPC absent unless explicitly enabled', async () => { + const fixture = await createGraphRpcFixture(); + const defaultHandlers = createRpcHandlers({ + coordinator: coordinator(), + cwd: fixture.cwd, + }); + const readOnlyHandlers = createReadOnlyRpcHandlers({ + coordinator: coordinator(), + cwd: fixture.cwd, + }); + const devHandlers = createRpcHandlers({ + coordinator: coordinator(), + cwd: fixture.cwd, + devRpc: true, + }); + + await expect( + defaultHandlers.handle({ jsonrpc: '2.0', id: 56, method: 'dev.graph.commitGraph', params: {} }), + ).resolves.toMatchObject({ + jsonrpc: '2.0', + id: 56, + error: { code: -32601, message: 'Method not found' }, + }); + + const defaultDiscovery = await defaultHandlers.handle({ jsonrpc: '2.0', id: 57, method: 'rpc.discover' }); + const readOnlyDiscovery = await readOnlyHandlers.handle({ + jsonrpc: '2.0', + id: 58, + method: 'rpc.discover', + }); + const devDiscovery = await devHandlers.handle({ jsonrpc: '2.0', id: 59, method: 'rpc.discover' }); + if (!('result' in defaultDiscovery) || !('result' in readOnlyDiscovery) || !('result' in devDiscovery)) { + throw new Error('expected discovery success'); + } + + const methodNames = (response: typeof defaultDiscovery) => + ( + response.result as { + methods: Array<{ method: string }>; + } + ).methods.map((entry) => entry.method); + + expect(methodNames(defaultDiscovery)).not.toContain('dev.graph.commitGraph'); + expect(methodNames(readOnlyDiscovery)).not.toContain('dev.graph.commitGraph'); + expect(methodNames(devDiscovery)).toContain('dev.graph.commitGraph'); + expect(JSON.stringify(devDiscovery.result)).toContain('existingCode'); + expect(JSON.stringify(devDiscovery.result)).toContain('explicit'); + expect(JSON.stringify(devDiscovery.result)).toContain('implicit'); + }); + + it('commits explicit graph batches through dev RPC and reads them back through public graph RPC', async () => { + const fixture = await createGraphRpcFixture(); + const productUpdates = createProductUpdatePublisher(); + const updates: unknown[] = []; + productUpdates.subscribe((batch) => updates.push(...batch)); + const handlers = createRpcHandlers({ + coordinator: coordinator(), + cwd: fixture.cwd, + productUpdates, + devRpc: true, + }); + + const response = await handlers.handle({ + jsonrpc: '2.0', + id: 60, + method: 'dev.graph.commitGraph', + params: { + specId: fixture.specAId, + basis: 'explicit', + nodes: [{ ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Dev RPC thesis' }], + edges: [{ category: 'support', source: { existingCode: 'R1' }, target: 'thesis', stance: 'for' }], + }, + }); + + expect(response).toMatchObject({ + jsonrpc: '2.0', + id: 60, + result: { + status: 'success', + lsn: expect.any(Number), + createdNodes: { thesis: { id: expect.any(Number), code: 'TH1' } }, + edges: [expect.any(Number)], + }, + }); + if (!('result' in response)) throw new Error('expected commit success'); + const commitResult = response.result as { readonly lsn: number }; + + expect(updates).toEqual([ + { topic: 'graph.overview', specId: fixture.specAId, lsn: commitResult.lsn }, + { topic: 'graph.nodeNeighborhood', specId: fixture.specAId, lsn: commitResult.lsn }, + ]); + + const overview = await handlers.handle({ + jsonrpc: '2.0', + id: 61, + method: 'graph.overview', + params: { specId: fixture.specAId }, + }); + expect(overview).toMatchObject({ + jsonrpc: '2.0', + id: 61, + result: { + nodeCount: 3, + edgeCount: 2, + lsn: commitResult.lsn, + nodes: expect.arrayContaining([expect.objectContaining({ title: 'Dev RPC thesis' })]), + edges: expect.arrayContaining([expect.objectContaining({ category: 'support', stance: 'for' })]), + }, + }); + }); + + it('rejects invalid dev graph commits without partial persistence', async () => { + const fixture = await createGraphRpcFixture(); + const handlers = createRpcHandlers({ + coordinator: coordinator(), + cwd: fixture.cwd, + devRpc: true, + }); + + await expect( + handlers.handle({ + jsonrpc: '2.0', + id: 62, + method: 'dev.graph.commitGraph', + params: { + specId: fixture.specAId, + basis: 'accepted_review_set', + nodes: [], + edges: [], + }, + }), + ).resolves.toMatchObject({ + jsonrpc: '2.0', + id: 62, + error: { code: -32602, message: 'Invalid params' }, + }); + + const invalid = await handlers.handle({ + jsonrpc: '2.0', + id: 63, + method: 'dev.graph.commitGraph', + params: { + specId: fixture.specAId, + basis: 'explicit', + nodes: [{ ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Invalid dev RPC thesis' }], + edges: [{ category: 'proof', source: { existingCode: 'R1' }, target: 'thesis' }], + }, + }); + + expect(invalid).toMatchObject({ + jsonrpc: '2.0', + id: 63, + result: { + status: 'structural_illegal', + diagnostics: expect.arrayContaining([ + expect.objectContaining({ field: 'edges[0].stance', message: expect.any(String) }), + ]), + }, + }); + + const overview = await handlers.handle({ + jsonrpc: '2.0', + id: 64, + method: 'graph.overview', + params: { specId: fixture.specAId }, + }); + expect(overview).toMatchObject({ + jsonrpc: '2.0', + id: 64, + result: { + nodeCount: 2, + edgeCount: 1, + lsn: fixture.finalLsn, + }, + }); + if (!('result' in overview)) throw new Error('expected overview success'); + expect(JSON.stringify(overview.result)).not.toContain('Invalid dev RPC thesis'); + }); + + it('enables dev graph commits over newline-delimited JSON-RPC streams', async () => { + const fixture = await createGraphRpcFixture(); + const input = new PassThrough(); + const output = new PassThrough(); + const productUpdates = createProductUpdatePublisher(); + const chunks: string[] = []; + output.on('data', (chunk) => chunks.push(String(chunk))); + + const done = runJsonRpcLineServer({ + input, + output, + handlers: createRpcHandlers({ + coordinator: coordinator(), + cwd: fixture.cwd, + productUpdates, + devRpc: true, + }), + productUpdates, + }); + + input.end( + [ + { + jsonrpc: '2.0', + id: 65, + method: 'dev.graph.commitGraph', + params: { + specId: fixture.specAId, + basis: 'explicit', + nodes: [{ ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Line RPC thesis' }], + edges: [{ category: 'support', source: { existingCode: 'R1' }, target: 'thesis', stance: 'for' }], + }, + }, + { jsonrpc: '2.0', id: 66, method: 'graph.overview', params: { specId: fixture.specAId } }, + ] + .map((message) => JSON.stringify(message)) + .join('\n') + '\n', + ); + await done; + + const messages = chunks + .join('') + .trim() + .split('\n') + .map((line) => JSON.parse(line) as Record); + expect(messages).toHaveLength(3); + expect(messages[0]).toMatchObject({ + jsonrpc: '2.0', + method: 'brunch.updated', + params: { + topics: ['graph.overview', 'graph.nodeNeighborhood'], + updates: [ + { topic: 'graph.overview', specId: fixture.specAId, lsn: expect.any(Number) }, + { topic: 'graph.nodeNeighborhood', specId: fixture.specAId, lsn: expect.any(Number) }, + ], + }, + }); + expect(messages[1]).toMatchObject({ + jsonrpc: '2.0', + id: 65, + result: { status: 'success', createdNodes: { thesis: { code: 'TH1' } } }, + }); + expect(JSON.stringify(messages[2])).toContain('Line RPC thesis'); + }); + it('returns parse errors over newline-delimited JSON-RPC streams', async () => { const input = new PassThrough(); const output = new PassThrough(); diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index 1082e0167..ad575f25b 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -7,6 +7,7 @@ import type { DefaultWorkspaceCoordinator, SpecSessionActivationCoordinator, } from '../session/workspace-session-coordinator.js'; +import { devGraphRpcMethods } from './methods/dev-graph.js'; import { graphRpcMethods } from './methods/graph.js'; import { discoverRpcMethods, @@ -44,8 +45,12 @@ export function createRpcHandlers(options: { coordinator: DefaultWorkspaceCoordinator & SpecSessionActivationCoordinator; cwd: string; productUpdates?: ProductUpdatePublisher; + devRpc?: boolean; }): RpcHandlers { - return createRpcHandlersForRegistry(options, FULL_RPC_METHOD_REGISTRY); + return createRpcHandlersForRegistry( + options, + options.devRpc ? [...FULL_RPC_METHOD_REGISTRY, ...devGraphRpcMethods] : FULL_RPC_METHOD_REGISTRY, + ); } function createRpcHandlersForRegistry( diff --git a/src/rpc/methods/dev-graph.ts b/src/rpc/methods/dev-graph.ts new file mode 100644 index 000000000..07d8f4f55 --- /dev/null +++ b/src/rpc/methods/dev-graph.ts @@ -0,0 +1,233 @@ +import { Type, type Static } from 'typebox'; +import { Value } from 'typebox/value'; + +import { + DESIGN_KINDS, + EDGE_CATEGORIES, + EDGE_STANCES, + INTENT_KINDS, + ORACLE_KINDS, + PLAN_KINDS, + parseGraphNodeCode, + type BatchEdgeInput, + type BatchEdgeRef, + type BatchNodeInput, + type CommitGraphInput, + type Diagnostic, + type StructuralIllegal, +} from '../../graph/index.js'; +import { graphMutationProductUpdates } from '../product-updates.js'; +import { createJsonRpcFailure, createJsonRpcSuccess, jsonRpcRequestId } from '../protocol.js'; +import type { RpcMethodContext, RpcMethodDefinition } from './registry.js'; +import { PositiveIntegerSchema } from './schemas.js'; + +const ALL_KINDS = [...INTENT_KINDS, ...ORACLE_KINDS, ...DESIGN_KINDS, ...PLAN_KINDS] as const; + +const BasisSchema = Type.Union([Type.Literal('explicit'), Type.Literal('implicit')]); +const NodePlaneSchema = Type.Union([ + Type.Literal('intent'), + Type.Literal('oracle'), + Type.Literal('design'), + Type.Literal('plan'), +]); +const NodeKindSchema = Type.Union(ALL_KINDS.map((kind) => Type.Literal(kind))); +const EdgeCategorySchema = Type.Union(EDGE_CATEGORIES.map((category) => Type.Literal(category))); +const EdgeStanceSchema = Type.Union(EDGE_STANCES.map((stance) => Type.Literal(stance))); + +const DevCommitNodeSchema = Type.Object( + { + ref: Type.String(), + plane: NodePlaneSchema, + kind: NodeKindSchema, + title: Type.String(), + body: Type.Optional(Type.String()), + source: Type.Optional(Type.String()), + detail: Type.Optional(Type.Unknown()), + }, + { additionalProperties: false }, +); + +const DevEdgeRefSchema = Type.Union([ + Type.String(), + Type.Object( + { + existingCode: Type.String({ + description: 'Projected node code resolved inside params.specId only, e.g. G1 or CON2.', + }), + }, + { additionalProperties: false }, + ), +]); + +const DevCommitEdgeSchema = Type.Object( + { + category: EdgeCategorySchema, + source: DevEdgeRefSchema, + target: DevEdgeRefSchema, + stance: Type.Optional(EdgeStanceSchema), + rationale: Type.Optional(Type.String()), + }, + { additionalProperties: false }, +); + +const DevCommitGraphParamsSchema = Type.Object( + { + specId: PositiveIntegerSchema, + basis: BasisSchema, + nodes: Type.Array(DevCommitNodeSchema), + edges: Type.Array(DevCommitEdgeSchema), + }, + { additionalProperties: false }, +); + +type DevCommitGraphParams = Static; + +type DevEdgeRef = Static; + +const DiagnosticSchema = Type.Object( + { + field: Type.String(), + message: Type.String(), + }, + { additionalProperties: false }, +); + +const DevCommitGraphResultSchema = Type.Union([ + Type.Object( + { + status: Type.Literal('success'), + lsn: Type.Number(), + createdNodes: Type.Record( + Type.String(), + Type.Object( + { + id: Type.Number(), + code: Type.String(), + }, + { additionalProperties: false }, + ), + ), + edges: Type.Array(Type.Number()), + }, + { additionalProperties: false }, + ), + Type.Object( + { + status: Type.Literal('structural_illegal'), + diagnostics: Type.Array(DiagnosticSchema), + }, + { additionalProperties: false }, + ), +]); + +export const devGraphRpcMethods: readonly RpcMethodDefinition[] = [ + { + method: 'dev.graph.commitGraph', + access: 'write', + description: + 'Dev-only local fixture-curation harness: atomically commit an explicit-spec graph batch through CommandExecutor.', + paramsSchema: DevCommitGraphParamsSchema, + resultSchema: DevCommitGraphResultSchema, + examples: [ + { + jsonrpc: '2.0', + id: 90, + method: 'dev.graph.commitGraph', + params: { + specId: 1, + basis: 'explicit', + nodes: [{ ref: 'n1', plane: 'intent', kind: 'thesis', title: 'Curated thesis' }], + edges: [{ category: 'support', source: { existingCode: 'G1' }, target: 'n1', stance: 'for' }], + }, + }, + ], + async handle(context, request) { + const requestId = jsonRpcRequestId(request); + const params = parseDevCommitGraphParams(request.params); + if (!params.ok) { + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); + } + + const graph = await context.getGraphRuntime(); + const input = translateDevCommitGraph(params.value, graph.forSpec(params.value.specId).resolveNodeCode); + const result = 'status' in input ? input : graph.commandExecutor.commitGraph(input); + if (result.status === 'success') { + context.productUpdates?.publish( + graphMutationProductUpdates({ specId: params.value.specId, lsn: result.lsn }), + ); + } + return createJsonRpcSuccess(requestId, result); + }, + }, +]; + +type DevCommitGraphParamsParseResult = + | { + ok: true; + value: DevCommitGraphParams; + } + | { ok: false }; + +function parseDevCommitGraphParams(value: unknown): DevCommitGraphParamsParseResult { + if (!Value.Check(DevCommitGraphParamsSchema, value)) return { ok: false }; + return { ok: true, value: Value.Parse(DevCommitGraphParamsSchema, value) }; +} + +function translateDevCommitGraph( + params: DevCommitGraphParams, + resolveNodeCode: (code: string) => number | undefined, +): CommitGraphInput | StructuralIllegal { + const diagnostics: Diagnostic[] = []; + const nodes: BatchNodeInput[] = params.nodes.map((node) => ({ + ref: node.ref, + plane: node.plane, + kind: node.kind, + title: node.title, + body: node.body, + source: node.source, + detail: node.detail, + })); + const edges: BatchEdgeInput[] = []; + + for (const [index, edge] of params.edges.entries()) { + const source = normalizeEdgeRef(edge.source, resolveNodeCode, `edges[${index}].source`, diagnostics); + const target = normalizeEdgeRef(edge.target, resolveNodeCode, `edges[${index}].target`, diagnostics); + if (source.status === 'invalid' || target.status === 'invalid') continue; + edges.push({ + category: edge.category, + source: source.ref, + target: target.ref, + stance: edge.stance, + rationale: edge.rationale, + }); + } + + if (diagnostics.length > 0) return { status: 'structural_illegal', diagnostics }; + return { specId: params.specId, basis: params.basis, nodes, edges }; +} + +type NormalizedEdgeRef = + | { readonly status: 'valid'; readonly ref: BatchEdgeRef } + | { readonly status: 'invalid' }; + +function normalizeEdgeRef( + ref: DevEdgeRef, + resolveNodeCode: (code: string) => number | undefined, + field: string, + diagnostics: Diagnostic[], +): NormalizedEdgeRef { + if (typeof ref === 'string') return { status: 'valid', ref }; + if (!parseGraphNodeCode(ref.existingCode)) { + diagnostics.push({ field, message: `malformed graph node code "${ref.existingCode}"` }); + return { status: 'invalid' }; + } + const nodeId = resolveNodeCode(ref.existingCode); + if (nodeId === undefined) { + diagnostics.push({ + field, + message: `graph node code "${ref.existingCode}" does not resolve in the selected spec`, + }); + return { status: 'invalid' }; + } + return { status: 'valid', ref: { existing: nodeId } }; +} diff --git a/src/rpc/methods/session.ts b/src/rpc/methods/session.ts index fa9404b39..616a68564 100644 --- a/src/rpc/methods/session.ts +++ b/src/rpc/methods/session.ts @@ -1,6 +1,9 @@ import { Type, type Static } from 'typebox'; import { Value } from 'typebox/value'; +import { captureStructuredResponseFacts } from '../../graph/capture/structured-response.js'; +import type { StructuredResponseCaptureOutcome } from '../../graph/capture/structured-response.js'; +import type { WorkspaceGraphRuntime } from '../../graph/workspace-store.js'; import { readBrunchSessionEnvelope, NonLinearTranscriptError, @@ -30,7 +33,11 @@ import type { WorkspaceActivationState, WorkspaceSessionState, } from '../../session/workspace-session-coordinator.js'; -import { selectedSessionProductUpdates, type ProductUpdatePublisher } from '../product-updates.js'; +import { + graphMutationProductUpdates, + selectedSessionProductUpdates, + type ProductUpdatePublisher, +} from '../product-updates.js'; import { createJsonRpcFailure, createJsonRpcSuccess, @@ -203,18 +210,47 @@ const ExchangeResponseParamsSchema = Type.Object( { additionalProperties: false }, ); +const ExchangeResponseCaptureResultSchema = Type.Union([ + Type.Object( + { + status: Type.Literal('captured'), + lsn: PositiveIntegerSchema, + nodeCount: NonNegativeIntegerSchema, + createdNodes: Type.Object({}, { additionalProperties: true }), + }, + { additionalProperties: false }, + ), + Type.Object( + { + status: Type.Literal('no_capture'), + reason: NonBlankStringSchema, + }, + { additionalProperties: false }, + ), + Type.Object( + { + status: Type.Literal('structural_illegal'), + diagnostics: Type.Array(Type.Object({}, { additionalProperties: true })), + }, + { additionalProperties: false }, + ), +]); + const ExchangeResponseResultSchema = Type.Object( { status: Type.Literal('accepted'), exchangeId: NonBlankStringSchema, answer: Type.Object({}, { additionalProperties: true }), + capture: ExchangeResponseCaptureResultSchema, note: Type.Optional(Type.String()), }, { additionalProperties: false }, ); type ExchangeResponseParams = StructuredExchangeResponseInput; -type ExchangeResponseResult = Static; +type ExchangeResponseResult = Omit, 'capture'> & { + readonly capture: StructuredResponseCaptureOutcome; +}; export const sessionRpcMethods: readonly RpcMethodDefinition[] = [ { @@ -417,6 +453,7 @@ async function handleSubmitExchangeResponse( coordinator: DefaultWorkspaceCoordinator; cwd: string; productUpdates?: ProductUpdatePublisher; + getGraphRuntime: () => Promise; }, ): Promise { if (!Value.Check(ExchangeResponseParamsSchema, rawParams)) { @@ -457,17 +494,31 @@ async function handleSubmitExchangeResponse( return createJsonRpcFailure(requestId, -32007, accepted.message); } + const graph = await options.getGraphRuntime(); + const capture = captureStructuredResponseFacts({ + specId: target.envelope.binding.specId, + exchangeId: pending.exchangeId, + answer: accepted.answer, + commandExecutor: graph.commandExecutor, + }); + const result: ExchangeResponseResult = { status: 'accepted', exchangeId: pending.exchangeId, answer: accepted.answer, + capture, ...(params.note === undefined ? {} : { note: params.note }), }; state.session.manager.appendMessage(accepted.toolResultMessage); flushSessionEntries(state.session.manager, state.session.file); - publishSelectedSessionUpdates(options.productUpdates, state); + publishSelectedSessionUpdates(options.productUpdates, state, target.envelope.binding.specId); + if (capture.status === 'captured') { + options.productUpdates?.publish( + graphMutationProductUpdates({ specId: target.envelope.binding.specId, lsn: capture.lsn }), + ); + } return createJsonRpcSuccess(requestId, result); } @@ -542,13 +593,14 @@ async function selectedSessionFile(state: WorkspaceSessionState): Promise { } }); + it('continues delivering product updates after a failed WebSocket request', async () => { + const productUpdates = createProductUpdatePublisher(); + const host = await startWebHost({ + cwd: '/tmp/brunch-project', + port: 0, + coordinator: throwingCoordinator(), + productUpdates, + }); + const observer = await openWebSocket(`${host.url.replace(/^http/u, 'ws')}/rpc`); + try { + const failedResponse = nextWebSocketMessage(observer); + observer.send( + JSON.stringify({ + jsonrpc: '2.0', + id: 14, + method: 'workspace.snapshot', + }), + ); + + await expect(failedResponse).resolves.toEqual({ + jsonrpc: '2.0', + id: 14, + error: { code: -32603, message: 'Internal error' }, + }); + + const notification = nextWebSocketMessage(observer); + productUpdates.publish({ topic: 'graph.overview', specId: 1, lsn: 8 }); + + await expect(notification).resolves.toMatchObject({ + jsonrpc: '2.0', + method: 'brunch.updated', + }); + } finally { + observer.close(); + await host.close(); + } + }); + it('rejects non-rpc WebSocket upgrade paths', async () => { const host = await startWebHost({ cwd: '/tmp/brunch-project', diff --git a/src/rpc/websocket.ts b/src/rpc/websocket.ts index e3494b700..ebe53a848 100644 --- a/src/rpc/websocket.ts +++ b/src/rpc/websocket.ts @@ -1,6 +1,6 @@ import type { Server as HttpServer } from 'node:http'; -import { WebSocketServer, type RawData } from 'ws'; +import { WebSocketServer, type RawData, type WebSocket } from 'ws'; import type { RpcHandlers } from './handlers.js'; import { createProductUpdateNotification, type ProductUpdatePublisher } from './product-updates.js'; @@ -48,14 +48,17 @@ export function attachWebRpcTransport(options: { webSocketServer.on('connection', (webSocket) => { webSocket.on('message', (data) => { - activeRequests += 1; - void handleMessage(options.handlers, data).then((response) => { - webSocket.send(JSON.stringify(response)); - activeRequests -= 1; - if (activeRequests === 0) { - flushDeferredNotifications(); - } - }); + recordRequestStarted(); + void handleMessage(options.handlers, data) + .catch(() => ({ + jsonrpc: '2.0' as const, + id: null, + error: { code: -32603, message: 'Internal error' }, + })) + .then((response) => { + sendRpcResponse(webSocket, response); + }) + .finally(recordRequestFinished); }); }); @@ -76,9 +79,34 @@ export function attachWebRpcTransport(options: { }); }, }; + function recordRequestStarted(): void { + activeRequests += 1; + } + + function recordRequestFinished(): void { + activeRequests -= 1; + if (activeRequests === 0) { + flushDeferredNotifications(); + } + } + + function sendRpcResponse(client: WebSocket, response: Awaited>): void { + sendIfOpen(client, JSON.stringify(response)); + } + function broadcastProductUpdate(notification: string): void { for (const client of webSocketServer.clients) { - client.send(notification); + sendIfOpen(client, notification); + } + } + + function sendIfOpen(client: WebSocket, message: string): void { + if (client.readyState !== client.OPEN) return; + try { + client.send(message); + } catch { + // Ignore per-client transport failures; other observers and request + // accounting must continue. } } } diff --git a/src/web/README.md b/src/web/README.md index 6fd120a14..95b6b4b2d 100644 --- a/src/web/README.md +++ b/src/web/README.md @@ -313,7 +313,7 @@ planned mutation hooks (not sidecar-accepted today): session.submitExchangeResponse submitExchangeResponseMutationOptions(rpc) - On success: invalidate session.pendingExchange, session.exchanges, session.runtimeState; graph projections update only after agent-internal graph commit publishes graph topics. + On success: invalidate session.pendingExchange, session.exchanges, session.runtimeState; captured labeled text answers also publish graph.overview / graph.nodeNeighborhood topics for the transcript-bound spec. reserved future method: session.submitMessage diff --git a/src/web/app.test.tsx b/src/web/app.test.tsx index a2c944f86..870bb0cf8 100644 --- a/src/web/app.test.tsx +++ b/src/web/app.test.tsx @@ -195,6 +195,44 @@ describe('Brunch React web app', () => { expect(calls).toEqual([{ method: 'graph.overview', params: { specId: 1 } }]); }); + it('ignores malformed product update entries instead of broadly invalidating graph reads', async () => { + window.history.pushState(null, '', '/spec/1'); + const calls: RpcCall[] = []; + const listeners = new Set(); + const runtime = createBrunchWebRuntime({ + rpcClient: rpcClient({ calls, listeners, graphOverview: populatedGraphOverview }), + }); + + render(); + + expect(await screen.findByText('Spec A requirement')).toBeTruthy(); + calls.length = 0; + for (const listener of listeners) { + listener({ + jsonrpc: '2.0', + method: 'brunch.updated', + params: { updates: [{ topic: 'graph.overview' }] }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(calls).toEqual([]); + }); + + it('rejects malformed spec route params before requesting a graph overview', async () => { + window.history.pushState(null, '', '/spec/not-a-spec-id'); + const calls: RpcCall[] = []; + const runtime = createBrunchWebRuntime({ + rpcClient: rpcClient({ calls, graphOverview: populatedGraphOverview }), + }); + + render(); + + expect(await screen.findByText('Invalid spec id.')).toBeTruthy(); + expect(calls).toContainEqual({ method: 'workspace.snapshot' }); + expect(calls.some((call) => call.method === 'graph.overview')).toBe(false); + }); + it('treats the spec route as client-local view state without borrowing the TUI session transcript', async () => { window.history.pushState(null, '', '/spec/2'); const calls: RpcCall[] = []; diff --git a/src/web/features/graph/GraphOverview.tsx b/src/web/features/graph/GraphOverview.tsx index 13386f022..7cf0dd86d 100644 --- a/src/web/features/graph/GraphOverview.tsx +++ b/src/web/features/graph/GraphOverview.tsx @@ -76,10 +76,15 @@ function groupNodes(nodes: GraphOverview['nodes']): Array<{ label: string; nodes: GraphOverview['nodes']; }> { - const groups = new Map(); + const groups = new Map>(); for (const node of nodes) { const label = `${node.plane} / ${node.kind}`; - groups.set(label, [...(groups.get(label) ?? []), node]); + const group = groups.get(label); + if (group) { + group.push(node); + } else { + groups.set(label, [node]); + } } return [...groups.entries()] .sort(([left], [right]) => left.localeCompare(right)) diff --git a/src/web/routes/spec.tsx b/src/web/routes/spec.tsx index 5d98ecace..6e94943d4 100644 --- a/src/web/routes/spec.tsx +++ b/src/web/routes/spec.tsx @@ -10,7 +10,10 @@ export const specRoute = createRoute({ getParentRoute: () => rootRoute, path: '/spec/$specId', loader: ({ context, params }) => { - const specId = Number(params.specId); + const specId = parseSpecRouteId(params.specId); + if (specId === undefined) { + return context.queryClient.ensureQueryData(workspaceSnapshotQueryOptions(context.rpcClient)); + } return Promise.all([ context.queryClient.ensureQueryData(workspaceSnapshotQueryOptions(context.rpcClient)), context.queryClient.ensureQueryData(graphOverviewQueryOptions(context.rpcClient, specId)), @@ -20,18 +23,41 @@ export const specRoute = createRoute({ }); function SpecRoutePage() { - const { rpcClient } = specRoute.useRouteContext(); const { specId } = specRoute.useParams(); - const parsedSpecId = Number(specId); + const parsedSpecId = parseSpecRouteId(specId); + if (parsedSpecId === undefined) return ; + return ; +} + +function InvalidSpecRoutePage() { + const { rpcClient } = specRoute.useRouteContext(); const { data: snapshot } = useSuspenseQuery(workspaceSnapshotQueryOptions(rpcClient)); - const { data: overview } = useSuspenseQuery(graphOverviewQueryOptions(rpcClient, parsedSpecId)); + return ( +
+

Brunch workspace

+ +

Invalid spec id.

+
+ ); +} + +function ValidSpecRoutePage({ specId }: { specId: number }) { + const { rpcClient } = specRoute.useRouteContext(); + const { data: snapshot } = useSuspenseQuery(workspaceSnapshotQueryOptions(rpcClient)); + const { data: overview } = useSuspenseQuery(graphOverviewQueryOptions(rpcClient, specId)); return (

Brunch workspace

- + - +
); } + +function parseSpecRouteId(value: string): number | undefined { + if (!/^[1-9]\d*$/u.test(value)) return undefined; + const specId = Number(value); + return Number.isSafeInteger(specId) ? specId : undefined; +} diff --git a/src/web/subscriptions/brunch-updates.ts b/src/web/subscriptions/brunch-updates.ts index 4aeb4475f..8c0dcac00 100644 --- a/src/web/subscriptions/brunch-updates.ts +++ b/src/web/subscriptions/brunch-updates.ts @@ -36,7 +36,9 @@ export function invalidateBrunchUpdate( const updates = Array.isArray(params.updates) ? params.updates : []; if (updates.length > 0) { for (const update of updates) { - invalidateProductUpdate(queryClient, update as ProductUpdate); + if (isProductUpdate(update)) { + invalidateProductUpdate(queryClient, update); + } } return; } @@ -96,6 +98,16 @@ function invalidateExact(queryClient: QueryClient, queryKey: QueryKey): void { void queryClient.invalidateQueries({ queryKey, exact: true }); } +function isProductUpdate(value: unknown): value is ProductUpdate { + if (!isRecord(value)) return false; + if (value.topic === 'workspace.snapshot') return true; + if (value.topic === 'graph.overview') return typeof value.specId === 'number'; + if (value.topic === 'graph.nodeNeighborhood') { + return typeof value.specId === 'number' && typeof value.nodeId === 'number'; + } + return typeof value.topic === 'string'; +} + function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; + return typeof value === 'object' && value !== null && !Array.isArray(value); }