diff --git a/.agents/skills/ln-consult/SKILL.md b/.agents/skills/ln-consult/SKILL.md index 8c5f55aa..6c8f3db0 100644 --- a/.agents/skills/ln-consult/SKILL.md +++ b/.agents/skills/ln-consult/SKILL.md @@ -30,7 +30,7 @@ Start the assessment with 2-4 bullets naming: - the active frontier item or nearby priority - volatile state or manual follow-up from handoff - the main open risk -- the cheapest tracer bullet that would score on proof of life, invariants, or uncertainty retirement (see `docs/praxis/ln-skills.md` §Tracer-bullet sequencing) +- the inherited **certainty posture** for the active frontier (see `docs/praxis/ln-skills.md` §Operating posture). Under `proving`, also name the cheapest tracer bullet that would score on proof of life, invariants, or uncertainty retirement. Under `earned`, name the closure target (what dual shape, ambiguity, or open decision does the next move close?). ## Work-type classification @@ -77,11 +77,15 @@ Only recommend the bounded or direct-build exceptions when all of these are true Only recommend the bounded serial exception when those same conditions hold and the next several commit-sized steps are obvious enough to queue without fresh planning. -## Tracer-bullet override +## Posture-aware route override -When several routes fit the work, prefer the one that fires the **tracer bullet that tells you the most**. A tracer-bullet slice scores on three convergent axes (see `docs/praxis/ln-skills.md` §Tracer-bullet sequencing): proof of life, invariants, uncertainty. The best next slice scores on more than one. +When several routes fit the work, the preferred route depends on the active frontier's certainty posture (see `docs/praxis/ln-skills.md` §Operating posture). -Given the repo's pre-release posture, attack uncertainty by building. Recommend a non-build route only when no buildable tracer bullet can carry the proof: +**Proving posture.** Prefer the route that fires the **tracer bullet that tells you the most**. A tracer-bullet slice scores on three convergent axes: proof of life, invariants, uncertainty. The best next slice scores on more than one. + +**Earned posture.** Prefer the route that lands the **closure that the recent slices have been deferring**. Closure slices answer: what dual shape closes, what topology materializes, what name canonicalizes, what carrier retires, what shape locks in. If the closure target is named and a single slice can land it, route directly to `ln-scope` / `ln-build` rather than to further planning. + +Under proving posture, attack uncertainty by building. Recommend a non-build route only when no buildable tracer bullet can carry the proof: - `ln-design` — module shape itself is uncertain and any slice would lock in the wrong seam - `ln-oracles` — verification is too uncertain to distinguish a passing slice from a wrong one diff --git a/.agents/skills/ln-plan/SKILL.md b/.agents/skills/ln-plan/SKILL.md index 32ba60e8..bfb9f55a 100644 --- a/.agents/skills/ln-plan/SKILL.md +++ b/.agents/skills/ln-plan/SKILL.md @@ -41,6 +41,23 @@ Archive deeper history to `docs/archive/PLAN_HISTORY.md` instead of keeping it l Treat frontier items as branch-sized work, not commit-sized work. If one frontier item will unfold as several consecutive verified slices, keep that chain in a `Mode: chain` scope file under `memory/cards/` or in session context instead of fragmenting `memory/PLAN.md` into a commit ledger. `memory/PLAN.md` may carry at most a lightweight pointer such as `current execution pointer: memory/cards/--.md`; detailed discretionary sub-slicing belongs in the scope file itself. +## Operating posture + +Sequencing pressure depends on the active frontier's **certainty posture**. Read `.pi/POSTURE.md` (if present) for the project default, then check each `Active` / `Next` frontier definition for an explicit `Certainty:` override. + +| Certainty | Ask | Optimize for | Reference | +| --- | --- | --- | --- | +| `proving` | What does landing this *tell us*? | information gain | [`references/proving.md`](references/proving.md) | +| `earned` | What does landing this *close*? | closure gain | [`references/earned.md`](references/earned.md) | + +The posture is **per frontier**, not per project. A mostly-earned repo can carry a fresh proving seam; a settled seam can regress to proving on a new unknown. The project posture in `.pi/POSTURE.md` is only the default — annotate the frontier when it diverges. + +Posture annotations are **required** on every `Active` / `Next` frontier (see the matching reference for the field set). If no posture-specific annotation applies, the frontier is not earning its slot — reshape, reclassify, or demote it. + +When implementation later reveals the posture was wrong, treat that as a state transition (downgrade earned → proving, reshape the slice, route back through `ln-plan` if the frontier itself splits). Do not invent a third permanent posture. + +Defensive parsing: depend primarily on `.pi/POSTURE.md`'s `certainty:` field; tolerate extra or mismatched fields rather than failing on schema drift. + ## Input The feature or project area: $ARGUMENTS @@ -98,35 +115,18 @@ When the meaning, acceptance, verification, traceability, or design-doc referenc When a frontier completes, remove it from `Sequencing`, add a terse `Recently Completed` entry, and archive older completion history if needed. Keep the definition only if it still carries live rationale for nearby work; otherwise archive/retire it. -### Epistemic horizon - -If live low-confidence assumptions block downstream work, stop the plan at that boundary. Plan spikes or thinner proving frontier items, not fantasy certainty. - -### Tracer-bullet sequencing - -Sequencing is not only seam-driven. A good tracer-bullet frontier scores on three convergent axes (see `docs/praxis/ln-skills.md` §Tracer-bullet sequencing): **proof of life**, **invariants**, **uncertainty**. The strongest next frontier scores on more than one. - -When ranking candidates, weigh: - -- **blast radius** if a load-bearing assumption turns out false -- **reversibility cost** if discovered late vs early -- **validation cost** (cheap slice vs expensive end-to-end rework) -- **load-bearingness** (how many active/next frontiers depend on it) - -Annotate each `Active` / `Next` frontier definition with the relevant axes when they are in play: +### Posture-dependent sequencing -- `Retires: ` — collapses the assumption by landing -- `Depends on: (validated enough)` — assumption must be settled first -- `Blocked by: ` — load-bearing; do not start until retired -- `Lights up: ` — establishes a new end-to-end path -- `Stabilizes: ` — locates or fixes structure others will aim from +Sequencing pressures and required annotation fields depend on the active frontier's posture: -**Spike exception.** Use `ln-spike` only when no buildable frontier could carry the proof. Do not insert ceremonial spikes when a tracer-bullet frontier exists. +- **Proving frontiers** → load [`references/proving.md`](references/proving.md). Covers tracer-bullet axes (proof of life, invariants, uncertainty), epistemic horizon, spike exception, reshape-don't-defer, and the `Retires` / `Depends on` / `Blocked by` / `Lights up` / `Stabilizes` annotation set. +- **Earned frontiers** → load [`references/earned.md`](references/earned.md). Covers the closure move-set (materialize, consolidate, name canonically, delete-as-progress, retire bridges, take-the-bigger-step), the "circling" recognition heuristic, sprawl guardrails, regression handling, and the `Closes` / `Materializes` / `Canonicalizes` / `Deletes/retires` / `Locks in` annotation set. -This sequencing pressure is distinct from "Epistemic horizon": that rule tells the planner to *stop* at fog; this rule tells the planner to **fire the tracer that tells you the most**. +A plan may contain a mix of postures across its `Active` / `Next` frontiers. Load both references when planning a mixed plan. ## Procedure +0. Read `.pi/POSTURE.md` if present for the project's default certainty posture. For each `Active` / `Next` frontier, check for an explicit `Certainty:` override and load the matching reference (`references/proving.md` or `references/earned.md`). Load both when the plan is mixed. 1. Read `memory/PLAN.md` if it exists. Identify existing frontier ids and retire/archive stale completed material into `docs/archive/PLAN_HISTORY.md`. 2. Read `memory/SPEC.md` if it exists. Pull only the live requirements, assumptions, decisions, and invariants that still constrain forward work. 3. Explore the codebase enough to understand real boundaries. diff --git a/.agents/skills/ln-plan/assets/plan-template.md b/.agents/skills/ln-plan/assets/plan-template.md index 37aad9d2..1ca0ab04 100644 --- a/.agents/skills/ln-plan/assets/plan-template.md +++ b/.agents/skills/ln-plan/assets/plan-template.md @@ -40,9 +40,13 @@ - **Name:** [Human-readable frontier name] - **Linear:** [FE-XXX if known, or `unassigned`] - **Kind:** [structural | bounded feature | hardening | bugfix | refactor] +- **Certainty:** [proving | earned — default inherits from `.pi/POSTURE.md`; annotate when this frontier diverges] - **Status:** [not-started | in-progress | branch-complete | blocked | done] - **Objective:** [what this frontier changes] - **Why now / unlocks:** [why this belongs on the frontier and what it unlocks] +- **Posture annotations:** [required — use the field set from the matching posture reference] + - Proving: one or more of `Retires:`, `Depends on:`, `Blocked by:`, `Lights up:`, `Stabilizes:` + - Earned: one or more of `Closes:`, `Materializes:`, `Canonicalizes:`, `Deletes / retires:`, `Locks in:` - **Acceptance:** [observable frontier-level outcome] - **Verification:** [inner / middle / outer summary] - **Cross-cutting obligations:** [optional: subsystem / invariant / verification-layer obligations this frontier must preserve or establish] diff --git a/.agents/skills/ln-plan/references/earned.md b/.agents/skills/ln-plan/references/earned.md new file mode 100644 index 00000000..d6918e61 --- /dev/null +++ b/.agents/skills/ln-plan/references/earned.md @@ -0,0 +1,69 @@ +# Planning posture: earned + +Load this reference when the active frontier item declares `Certainty: earned`, or when the project's `.pi/POSTURE.md` declares `certainty: earned` and the frontier inherits. + +## Objective function + +Optimize for **closure gain**. The next frontier should *land and lock in* something the codebase has already proved out. Landing is valuable when it eliminates a dual shape, hardens a settled decision into topology, consolidates the lexicon, or retires an obsolete carrier. + +This is not "proving-posture sequencing with bigger steps." The decision kernel changes. The planner is no longer asking *"what does landing this tell us?"* — it is asking *"what does landing this close?"* + +## Closure move-set + +- **Materialize** — make a settled architectural decision visible in topology: file or directory placement, sub-tree split per [AGENTS.md](../../../../AGENTS.md) §fractal sub-tree pattern, or a topology README that locks a SPEC decision to a directory. +- **Consolidate** — bring scattered cognates of the same concept into one canonical site. +- **Name canonically** — collapse aliases, near-synonyms, or drift terms to one term; update callers, docs, and tests in the same slice. +- **Delete-as-progress** — retire obsolete code paths, fixtures, dummy data, compatibility shims, and superseded docs. Deletion is a first-class closure outcome, not janitorial overflow. +- **Retire bridges / aliases / dual paths** — under `migration: free-rewrite`, eliminate adapters, shims, and expand/contract scaffolds that have outlived their crossing. The migration scheme is not the system. (See `~/.pi/agent/APPEND_SYSTEM.md` §bridge-as-permanence.) +- **Take the bigger step** — landing a multi-file or multi-layer closure in one slice when the thinness instinct is producing redundant proof rather than closure. + +## Required annotation fields + +Every `Active` / `Next` frontier under earned posture must carry at least one of: + +- `Closes: ` — what becomes no-longer-open after landing +- `Materializes: ` — what gets embedded into topology +- `Canonicalizes: ` — what becomes the single canonical site +- `Deletes / retires: ` — what goes away +- `Locks in: ` — completion test for the closure + +`Locks in` is the completion test, not the action: it answers *"what is true after this lands that was previously open?"* + +## Recognition heuristic: circling + +You are circling, not landing, when: + +- Each new slice attaches an incremental proof to changes whose meaning is already established. +- The slice's tests rephrase what previous slices already showed. +- The planner is still maximizing tracer axes (`Lights up`, `Stabilizes`, `Retires`) on a seam where nothing material is unknown. +- "Caution" is the planner's stated reason, but no specific risk can be named that would shift the next move. + +When this pattern appears, switch posture on the frontier and plan the closure move that the proving slices have been deferring. + +## Guardrails + +The earned posture is not a license for sprawl. Closure expands the slice *within* a defined scope; it does not expand the scope. + +- **Stay inside one named seam or frontier.** "Take the bigger step" widens the work within a defined boundary; it does not redraw the boundary. +- **Name the specific closure target** in the frontier definition. "Tidy up X" is not a closure target; "collapse the dual `Foo` shapes to the `Foo` defined at `src/.../foo.ts`" is. +- **Declare touched paths** at the scope-card layer with the same discipline as proving-mode slices. Bigger does not mean undeclared. +- **Do not auto-implement adjacent work** because it would be "symmetric." Name adjacent work in the plan; let it earn its own frontier. +- **Materialization is not ritual.** Topology READMEs and fractal sub-tree splits only fire when (a) the seam is already understood, (b) the structure carries real architectural meaning, and (c) the change reduces ambiguity or drift. Otherwise it is structural theatre. + +## Regression: earned → proving + +When implementation reveals a real unknown that the closure depended on, do **not** invent a third posture mode. Transition the frontier: + +1. Downgrade the frontier (or the active slice within it) to `Certainty: proving`. +2. Reshape the slice as a tracer that retires the new unknown. +3. If the unknown forces the frontier itself to split or reorder, route back through `ln-plan`. + +The transition is the honest move; carrying earned posture over fog is the dishonest one. + +## Boundary with adjacent skills + +`ln-plan` owns closure as **intent**: what must be closed, which dual shapes must disappear, where topology and lexicon must harden, which bridges retire. + +`ln-refactor` owns closure as **safe mechanics**: when an earned frontier's execution is principally restructuring, the refactor plan sequences tiny behavior-preserving commits to land it. + +`ln-sync` owns closure as **canonical garbage collection**: stale docs, exhausted scope cards, derivative artifacts the planner is already done with. Closure work that is part of a frontier's definition of done belongs in `ln-plan`; cleanup of finished artifacts belongs in `ln-sync`. diff --git a/.agents/skills/ln-plan/references/proving.md b/.agents/skills/ln-plan/references/proving.md new file mode 100644 index 00000000..dfc3cd80 --- /dev/null +++ b/.agents/skills/ln-plan/references/proving.md @@ -0,0 +1,56 @@ +# Planning posture: proving + +Load this reference when the active frontier item declares `Certainty: proving`, or when the project's `.pi/POSTURE.md` declares `certainty: proving` and the frontier inherits. + +## Objective function + +Optimize for **information gain**. The next frontier should *tell you the most* about what is still unknown. Landing is valuable when it falsifies, retires, or locates a load-bearing belief — not when it merely produces visible output. + +## Tracer-bullet sequencing + +A good tracer-bullet frontier scores on at least one of three convergent axes: + +- **Proof of life.** Landing it lights up an end-to-end path that did not exist. +- **Invariants.** Landing it locates or stabilizes a seam that future slices will aim from. +- **Uncertainty.** Landing it retires a load-bearing assumption from `memory/SPEC.md` §Assumptions. + +The strongest next frontier scores on more than one axis. Prefer a slice that does several at once over one that maximizes a single axis. + +When ranking candidates, weigh: + +- **blast radius** if a load-bearing assumption turns out false +- **reversibility cost** if discovered late vs early +- **validation cost** (cheap slice vs expensive end-to-end rework) +- **load-bearingness** (how many active/next frontiers depend on it) + +## Required annotation fields + +Every `Active` / `Next` frontier under proving posture must carry at least one of: + +- `Retires: ` — collapses the assumption by landing +- `Depends on: (validated enough)` — assumption must be settled first +- `Blocked by: ` — load-bearing; do not start until retired +- `Lights up: ` — establishes a new end-to-end path +- `Stabilizes: ` — locates or fixes structure others will aim from + +If none of these apply, the frontier is not earning its slot under proving posture. Either reshape it, demote it to `Horizon`, or reclassify — it may actually be earned-posture work mislabelled as proving. + +## Epistemic horizon + +If live low-confidence assumptions block downstream work, **stop the plan at that boundary**. Plan spikes or thinner proving frontier items, not fantasy certainty. Sequencing past fog is the most expensive form of premature commitment. + +## Reshape, don't defer + +If an assumption blocks a slice, reshape the slice before switching to study. A tracer bullet that breaks when the assumption is wrong almost always beats a study step in this codebase. + +"High-impact" means the assumption being false would force rework across more than the slice — invalidating queued cards, changing the chosen module shape from `ln-design`, or forcing a different frontier-level sequencing decision. + +## Spike exception + +Use `ln-spike` only when no buildable frontier could carry the proof more cheaply — a third-party API contract, vendor performance characteristic, or research-grade unknown. Do not insert ceremonial spikes when a tracer-bullet frontier exists. + +## Fire the tracer that tells you the most + +Under proving posture, attack uncertainty by building. Spikes, design passes, and prototypes are escape hatches; the default is a slice whose landing falsifies the load-bearing belief. + +This sequencing pressure is distinct from the **Epistemic horizon** rule above. Horizon tells the planner to *stop* at fog; this rule tells the planner to **fire the tracer that retires the next fog patch**. diff --git a/.agents/skills/ln-scope/SKILL.md b/.agents/skills/ln-scope/SKILL.md index 214f98bd..0c2ead83 100644 --- a/.agents/skills/ln-scope/SKILL.md +++ b/.agents/skills/ln-scope/SKILL.md @@ -34,6 +34,8 @@ If this is a fresh thread or an unfamiliar area, also read `HANDOFF.md` if prese Write a 2-4 bullet orientation note naming the containing seam, the relevant frontier item, volatile handoff state, and the main open risk. Also name any frontier-level cross-cutting obligations that this slice must preserve or establish (for example a shared command-layer invariant, a side-task/event-substrate rule, or a replay/property/adversarial verification layer). +Name the inherited **certainty posture** explicitly: `Posture: proving (inherited from )` or `Posture: earned (inherited from )`. If scoping reveals the posture is wrong for this slice (most commonly: an earned frontier surfaces a real unknown), downgrade to `proving` and route back through `ln-plan` if the frontier definition itself must shift. Do not silently scope earned-mode slices over fog. + Do not create new planning documents or scratch scope stores without explicit permission. The canonical planning state remains `memory/SPEC.md` and `memory/PLAN.md`. The sanctioned derivative location for scope cards is `memory/cards/`, described below. If scoping reveals that one frontier item needs multiple sequential slices, keep them nested under that same frontier item unless the plan-level frontier must change. Do not silently turn slices into separate tracker / branch work items. @@ -159,9 +161,11 @@ Every boundary the slice passes through, entry to exit: → [→ memory/SPEC.md §Assumptions id] ``` -### Tracer-bullet check +### Posture check + +Apply the check matching the inherited certainty posture. See [`ln-plan/references/proving.md`](../ln-plan/references/proving.md) and [`ln-plan/references/earned.md`](../ln-plan/references/earned.md) for the full posture doctrine. -A good tracer-bullet slice scores on at least one of three convergent axes (see `docs/praxis/ln-skills.md` §Tracer-bullet sequencing): **proof of life** (lights up a new end-to-end path), **invariants** (locates or stabilizes a seam), **uncertainty** (retires a load-bearing assumption from `memory/SPEC.md` §Assumptions). The best slices score on more than one. +**Proving posture.** A good tracer-bullet slice scores on at least one of three convergent axes: **proof of life** (lights up a new end-to-end path), **invariants** (locates or stabilizes a seam), **uncertainty** (retires a load-bearing assumption from `memory/SPEC.md` §Assumptions). The best slices score on more than one. If the slice depends on a high-impact assumption that landing it will not retire: @@ -172,6 +176,20 @@ If the slice depends on a high-impact assumption that landing it will not retire A tracer bullet should *tell you something*. Build it. +**Earned posture.** A good closure slice answers at least one of: + +- What dual shape, ambiguity, or open decision does landing this **close**? +- What settled decision does it **materialize** into topology (file/directory placement, sub-tree split, topology README)? +- What term, API, or location does it **canonicalize**? +- What obsolete code path, fixture, doc, or bridge does it **delete / retire**? +- What invariant, contract, or shape does it **lock in** as the completion test? + +If the answer is "none of these — it just incrementally proves something already proved," you are circling. Either reshape the slice into a closure move, or recognize that the frontier itself has become an earned closure that the proving slices have been deferring. + +Earned slices may legitimately span multiple files or layers — "take the bigger step" is licensed under earned posture — but the guardrails in `references/earned.md` still bind: one named seam, named closure target, declared touched paths, no auto-implementation of adjacent work. + +If scoping surfaces a real unknown that closure depended on, downgrade the slice to proving and re-run the proving branch above. + ### Acceptance Criteria ``` @@ -254,7 +272,7 @@ State one of: - `None` — this slice's correctness does not hinge on any live `memory/SPEC.md` §Assumptions - `Depends on: ` — and a one-line note on why those assumptions are validated enough to build against -If a light card would have to mark `Depends on:` a high-impact unvalidated assumption, promote to a full scope card and apply the **Tracer-bullet check**. +If a light card would have to mark `Depends on:` a high-impact unvalidated assumption, promote to a full scope card and apply the **Posture check** (the proving-posture branch in particular). ### Expected touched paths (tentative) @@ -303,6 +321,6 @@ After the scope file is complete, present these options to the user (use `tool-a | 5 | Revise plan | `ln-plan` | The work no longer fits the current frontier | | 6 | Back to triage | `ln-consult` | Scope revealed unclear state | -Recommended: **1** in nearly all cases — including when the **Tracer-bullet check** fires, because the preferred resolution is to reshape, not defer. Recommend **3 (Spike first)** only when no vertical slice would be cheaper than a pure probe. Recommend **2 (Design oracles)** only when verification for the reshaped slice is still genuinely unclear. +Recommended: **1** in nearly all cases — including when the **Posture check** fires under proving posture, because the preferred resolution is to reshape, not defer. Under earned posture, recommend **1** when the closure target is named and the slice answers at least one closure question; recommend **5 (Revise plan)** when the slice exposes that the frontier itself has become a different closure than the plan describes. Recommend **3 (Spike first)** only when no vertical slice would be cheaper than a pure probe. Recommend **2 (Design oracles)** only when verification for the reshaped slice is still genuinely unclear. When routing to `ln-build`, name the scope file path explicitly (for example: "build `memory/cards/--.md`"). `ln-build` uses a hybrid selection policy and prefers an explicit path argument. diff --git a/.fixtures/runs/agents-composition-layer/runtime-posture-proof.md b/.fixtures/runs/agents-composition-layer/runtime-posture-proof.md new file mode 100644 index 00000000..89ab4abb --- /dev/null +++ b/.fixtures/runs/agents-composition-layer/runtime-posture-proof.md @@ -0,0 +1,14 @@ +# FE-806 runtime-posture proof + +Deterministic product-path proof: `src/.pi/__tests__/prompting.test.ts` drives `createBrunchPiExtensionShell()` through `before_agent_start` with transcript-backed `brunch.agent_runtime_state` switches. + +Contrasts recorded by the test: + +- `step-wise-disambiguate` + `intent` pins the strategy manifest to `step-wise-disambiguate` and renders intent-lens selected-spec graph context. +- `propose-graph` + `design` pins the strategy manifest to `propose-graph` and renders design-lens selected-spec graph context over the same snapshot. + +Accepted blind spots: + +- Prompt/body quality is fitness evidence, not this deterministic merge gate. +- Graph-write reliability remains with `graph-tool-resilience`. +- Capture quality remains with `capture-response-to-graph`. diff --git a/.fixtures/seed-specs/bilal-port/README.md b/.fixtures/seed-specs/bilal-port/README.md new file mode 100644 index 00000000..6142f704 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/README.md @@ -0,0 +1,63 @@ +# `.fixtures/seed-specs/bilal-port/` + +Ported spec graphs from Bilal's spec-elicitation prototype, transformed +to the brunch graph model. Intended as development seed data — rich, +real spec material to populate a dev SQLite database for UI / agent work. + +Not probe-run artifacts; sits alongside `.fixtures/runs/` rather than inside it. + +## Provenance + +Source: `/Users/lunelson/Code/hashintel/bilal-spec-elicitation-proto/spec//graph/` + +Generated by [`_port-script.ts`](./_port-script.ts) (co-located in this directory). +Re-runnable; each run wipes and re-emits per-spec subdirectories. + +## Transformation rules + +See the header docstring of the port script for the full mapping rules, +including: decision-hub-and-spoke collapse, justification-hub absorption, +evidence → oracle plane (with one synthetic per-spec `check`), +`risk` and `design` → `context` with source flags for curation, +and the `derived_from` → dependency-vs-support rule keyed on target kind. + +Curation flags carried in the `source` field: + +- `derived-risk-or-question` — was Bilal `risk` semanticRole; many are + literally "Open question (Q##): ..." phrased; per the interrogative + normalization rule in `docs/design/GRAPH_MODEL.md`, curate into + `assumption`, `criterion`, or keep as `context`. +- `derived-design-statement` — was Bilal `design` semanticRole; lacks + the structural material to prove a real decision/module; curate into + `decision` (if alternatives recoverable from history), or design plane + `module`/`interface` (if it actually names code). +- `derived-justification-synthesis` — was a Bilal `hub:justification`; + rationale appended to body. Curate per case. +- `derived-port-synthetic` — node minted by the port script itself + (currently only the per-spec audit `check`). + +## Output layout + +``` +bilal-port/ +├── README.md # this file (generated) +├── _port-script.ts # the porting script itself (re-runnable) +├── / +│ ├── spec.json # → specs table seed row +│ ├── nodes.json # → nodes table seed rows (local_id placeholder for autoincrement) +│ └── edges.json # → edges table seed rows (source/target reference local_id) +``` + +Field shape mirrors [`src/db/schema.ts`](../../../src/db/schema.ts) column names +(plane, kind, title, body, basis, source, detail). +No LSNs or change-log entries are pre-baked — a seed loader is expected +to wrap inserts in one `commitGraph`-style transaction so the graph clock, +change log, and lsn 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 | diff --git a/.fixtures/seed-specs/bilal-port/_port-script.ts b/.fixtures/seed-specs/bilal-port/_port-script.ts new file mode 100644 index 00000000..71bd6e04 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/_port-script.ts @@ -0,0 +1,777 @@ +/** + * One-off port of Bilal's spec-elicitation-prototype graph data into + * brunch-shaped node/edge JSON fixtures. + * + * Lives co-located with its output under .fixtures/seed-specs/bilal-port/ + * (the .fixtures/** tree is excluded from oxlint/oxfmt by project config — + * this script does not pass through the project verification harness). + * + * Run with: + * npx tsx .fixtures/seed-specs/bilal-port/_port-script.ts + * + * Source (read-only): + * /Users/lunelson/Code/hashintel/bilal-spec-elicitation-proto/spec//graph/{nodes,edges}.json + * + * Output (sibling per-spec subdirectories): + * .fixtures/seed-specs/bilal-port//{nodes,edges,spec}.json + * + * Mapping rules (derived in thread T-019e91ee, summarized below): + * + * Decision hubs (kind=hub, hubType=decision) collapse with their + * alternative-spoke neighbors into a single brunch `decision` node + * with detail.{chosen_option, rejected[], rationale}. The selected + * alternative becomes chosen_option; rejected+considered alternatives + * merge into rejected[]; the hub's rationale string becomes + * detail.rationale. + * + * Justification hubs (kind=hub, hubType=justification) emit as + * `context` nodes — the synthesized claim is the title/body, the + * long-form rationale is appended to body. Their incoming/outgoing + * edges port normally. + * + * Content nodes map by semanticRole: + * goal, context, term, constraint, requirement, criterion + * → intent plane / same kind (verbatim) + * evidence → oracle plane / evidence + * (plus one synthetic check per spec, "Code-audit pass", as the + * realization parent of every evidence node) + * risk → intent plane / context (per oracle guidance: blanket + * risk→assumption would falsify graph mechanics; context is the + * safe last-resort bucket. Source field flags for curation.) + * design → intent plane / context with source flag for curation + * (most are actually decisions or modules but lack the structural + * material to prove it; flagged as 'derived-design-statement') + * alternative → absorbed into parent decision (never emitted) + * + * Edge type → brunch category: + * considered, rejected, selected → absorbed (never emitted) + * informed_by → support[for] + * produced → realization + * consequence → dependency (source = cause/upstream) + * derived_from → dependency if target kind is structural-decisional + * (context, term, constraint, decision, goal, thesis, + * requirement, criterion); + * else support[for] (for observational targets: + * evidence, assumption, context-as-risk-rewrite) + * + * Field translation: + * authority → source ("stakeholder" | "technical" | "external" | + * "derived") + * epistemicStatus: + * inferred → basis: "accepted_review_set" + * asserted, observed, assumed → basis: "explicit" + * (epistemic flavor concatenated to source: e.g. + * "stakeholder-observed", "external-asserted") + * displayId → preserved as bracket suffix in source: "stakeholder [Q9]" + * + * Discarded: phase, frameId, lifecycle (all active), reviewStatus + * (all clean), provenance (already empty), createdAt. + * + * Re-run safely: output dir is wiped per spec on each run. + * + * Note: this script reads from an absolute external path on the + * author's machine. It is not portable across machines without + * adjusting BILAL_ROOT or providing the source data at that path. + */ + +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const BILAL_ROOT = '/Users/lunelson/Code/hashintel/bilal-spec-elicitation-proto/spec'; +const OUTPUT_ROOT = SCRIPT_DIR; + +const SPECS: { source: string; slug: string; displayName: string }[] = [ + { source: 'code-health', slug: 'code-health', displayName: 'Code Health' }, + { source: 'explorer-ui', slug: 'explorer-ui', displayName: 'Explorer UI' }, + { source: 'macro-view', slug: 'macro-view', displayName: 'Macro View' }, +]; + +// --------------------------------------------------------------------------- +// Source shape (Bilal) +// --------------------------------------------------------------------------- + +type BilalSemanticRole = + | 'goal' + | 'context' + | 'term' + | 'constraint' + | 'requirement' + | 'criterion' + | 'evidence' + | 'risk' + | 'design' + | 'alternative'; + +interface BilalNode { + id: string; // uuid + displayId: string; + specId: string; + frameId: string; + phase: string; + text: string; + lifecycle: string; + reviewStatus: { _tag: string }; + provenance: unknown[]; + createdAt: string; + kind: 'content' | 'hub'; + semanticRole?: BilalSemanticRole | null; + epistemicStatus?: 'asserted' | 'inferred' | 'observed' | 'assumed' | null; + authority?: 'external' | 'stakeholder' | 'technical' | 'derived' | null; + hubType?: 'decision' | 'justification' | null; + rationale?: string | null; +} + +type BilalEdgeType = + | 'derived_from' + | 'considered' + | 'rejected' + | 'selected' + | 'informed_by' + | 'consequence' + | 'produced'; + +interface BilalEdge { + id: string; + source: { specId: string; nodeId: string }; + target: { specId: string; nodeId: string }; + type: BilalEdgeType; + rationale: string | null; + provenance: unknown[]; + createdAt: string; +} + +// --------------------------------------------------------------------------- +// Output shape (brunch-flavored — column names match src/db/schema.ts; +// integer IDs are local-to-spec and treated as placeholders for +// autoincrement at load time) +// --------------------------------------------------------------------------- + +type Plane = 'intent' | 'oracle' | 'design' | 'plan'; + +interface BrunchNodeFixture { + local_id: number; + plane: Plane; + kind: string; + title: string; + body: string | null; + basis: 'explicit' | 'accepted_review_set'; + source: string | null; + detail: Record | null; +} + +interface BrunchEdgeFixture { + category: + | 'dependency' + | 'proof' + | 'support' + | 'realization' + | 'boundary' + | 'composition' + | 'association' + | 'supersession'; + source_local_id: number; + target_local_id: number; + stance: 'for' | 'against' | null; + basis: 'explicit' | 'accepted_review_set'; + rationale: string | null; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Brunch decision-detail target kinds (drive derived_from → dependency vs support). */ +const STRUCTURAL_DECISIONAL_KINDS = new Set([ + 'goal', + 'thesis', + 'term', + 'context', + 'requirement', + 'constraint', + 'invariant', + 'decision', + 'criterion', +]); + +/** First sentence (≤ N chars) used as a node title when full text is long. */ +function deriveTitle(text: string, max = 140): string { + const trimmed = text.trim(); + const firstSentenceEnd = trimmed.search(/[.!?](\s|$)/); + const candidate = firstSentenceEnd > 0 ? trimmed.slice(0, firstSentenceEnd + 1) : trimmed; + if (candidate.length <= max) return candidate; + return candidate.slice(0, max - 1).trimEnd() + '…'; +} + +/** Project Bilal authority + epistemicStatus into brunch source + basis. */ +function projectProvenance(node: BilalNode): { + basis: 'explicit' | 'accepted_review_set'; + source: string | null; +} { + const basis: 'explicit' | 'accepted_review_set' = + node.epistemicStatus === 'inferred' ? 'accepted_review_set' : 'explicit'; + + const parts: string[] = []; + if (node.authority) parts.push(node.authority); + if (node.epistemicStatus && node.epistemicStatus !== 'asserted') { + parts.push(node.epistemicStatus); + } + const flavor = parts.join('-'); + const tag = node.displayId ? ` [${node.displayId}]` : ''; + const source = flavor || tag ? `${flavor}${tag}`.trim() : null; + return { basis, source }; +} + +// --------------------------------------------------------------------------- +// Decision hub collapse +// --------------------------------------------------------------------------- + +interface DecisionCluster { + hubId: string; + hubNode: BilalNode; + selectedAltIds: string[]; + rejectedAltIds: string[]; + consideredAltIds: string[]; +} + +/** Index decision hubs and their spoke alternatives. */ +function buildDecisionClusters( + nodes: BilalNode[], + edges: BilalEdge[], +): { clusters: Map; absorbedAltIds: Set; absorbedEdgeIds: Set } { + const clusters = new Map(); + const absorbedAltIds = new Set(); + const absorbedEdgeIds = new Set(); + + const hubIds = new Set(nodes.filter((n) => n.kind === 'hub' && n.hubType === 'decision').map((n) => n.id)); + + for (const hubId of hubIds) { + const hubNode = nodes.find((n) => n.id === hubId); + if (!hubNode) continue; + clusters.set(hubId, { + hubId, + hubNode, + selectedAltIds: [], + rejectedAltIds: [], + consideredAltIds: [], + }); + } + + // Bilal's spoke-edge direction is asymmetric: + // selected, rejected: source = hub, target = alternative + // considered: source = alternative, target = hub + // Handle both directions defensively for all three types. + for (const edge of edges) { + if (edge.type !== 'selected' && edge.type !== 'rejected' && edge.type !== 'considered') continue; + + const fromHubCluster = clusters.get(edge.source.nodeId); + const toHubCluster = clusters.get(edge.target.nodeId); + const cluster = fromHubCluster ?? toHubCluster; + if (!cluster) continue; + const altId = fromHubCluster ? edge.target.nodeId : edge.source.nodeId; + + if (edge.type === 'selected') cluster.selectedAltIds.push(altId); + else if (edge.type === 'rejected') cluster.rejectedAltIds.push(altId); + else cluster.consideredAltIds.push(altId); + + absorbedAltIds.add(altId); + absorbedEdgeIds.add(edge.id); + } + + return { clusters, absorbedAltIds, absorbedEdgeIds }; +} + +// --------------------------------------------------------------------------- +// Classification: bilal node → brunch (plane, kind, body, detail) +// --------------------------------------------------------------------------- + +interface BrunchClassification { + plane: Plane; + kind: string; + title: string; + body: string | null; + detail: Record | null; + sourceFlag?: string; +} + +function classifyContentNode(node: BilalNode): BrunchClassification | null { + const role = node.semanticRole; + const text = node.text; + + switch (role) { + case 'goal': + case 'context': + case 'constraint': + case 'requirement': + case 'criterion': + return { + plane: 'intent', + kind: role, + title: deriveTitle(text), + body: text, + detail: null, + }; + + case 'term': + // brunch requires detail.definition for term nodes + return { + plane: 'intent', + kind: 'term', + title: deriveTitle(text, 80), + body: null, + detail: { definition: text }, + }; + + case 'evidence': + return { + plane: 'oracle', + kind: 'evidence', + title: deriveTitle(text), + body: text, + detail: null, + }; + + case 'risk': + return { + plane: 'intent', + kind: 'context', + title: deriveTitle(text), + body: text, + detail: null, + sourceFlag: 'derived-risk-or-question', + }; + + case 'design': + return { + plane: 'intent', + kind: 'context', + title: deriveTitle(text), + body: text, + detail: null, + sourceFlag: 'derived-design-statement', + }; + + case 'alternative': + // absorbed at decision-cluster phase; should not reach here + return null; + + default: + return null; + } +} + +function classifyDecisionCluster( + cluster: DecisionCluster, + altNodeIndex: Map, +): BrunchClassification { + const hub = cluster.hubNode; + const selectedNode = cluster.selectedAltIds + .map((id) => altNodeIndex.get(id)) + .find((n): n is BilalNode => Boolean(n)); + + const rejectedSet = new Set(); + for (const id of [...cluster.rejectedAltIds, ...cluster.consideredAltIds]) { + if (cluster.selectedAltIds.includes(id)) continue; + const altNode = altNodeIndex.get(id); + if (!altNode) continue; + rejectedSet.add(altNode.text); + } + const rejected = [...rejectedSet]; + if (rejected.length === 0) { + // hub had only selected alternatives; brunch requires ≥1 rejected. + // mark explicitly so curation can find these. + rejected.push('(no alternatives recorded in source data)'); + } + + const chosenOption = selectedNode ? selectedNode.text : hub.text; + + return { + plane: 'intent', + kind: 'decision', + title: deriveTitle(hub.text), + body: hub.text, + detail: { + chosen_option: chosenOption, + rejected, + rationale: hub.rationale ?? '', + }, + }; +} + +function classifyJustificationHub(node: BilalNode): BrunchClassification { + const bodyParts: string[] = [node.text]; + if (node.rationale) bodyParts.push('', '## Rationale', '', node.rationale); + return { + plane: 'intent', + kind: 'context', + title: deriveTitle(node.text), + body: bodyParts.join('\n'), + detail: null, + sourceFlag: 'derived-justification-synthesis', + }; +} + +// --------------------------------------------------------------------------- +// Edge mapping +// --------------------------------------------------------------------------- + +interface EdgeMapping { + category: BrunchEdgeFixture['category']; + stance: 'for' | 'against' | null; +} + +function mapEdge(edge: BilalEdge, targetBrunchKind: string | null): EdgeMapping | null { + switch (edge.type) { + case 'considered': + case 'rejected': + case 'selected': + return null; // absorbed + + case 'informed_by': + return { category: 'support', stance: 'for' }; + + case 'produced': + return { category: 'realization', stance: null }; + + case 'consequence': + // bilal: source caused target. brunch: source(dependency) → target(dependent) + return { category: 'dependency', stance: null }; + + case 'derived_from': + if (targetBrunchKind && STRUCTURAL_DECISIONAL_KINDS.has(targetBrunchKind)) { + return { category: 'dependency', stance: null }; + } + return { category: 'support', stance: 'for' }; + + default: + return null; + } +} + +// --------------------------------------------------------------------------- +// Per-spec porter +// --------------------------------------------------------------------------- + +interface SpecPortResult { + slug: string; + brunchNodes: BrunchNodeFixture[]; + brunchEdges: BrunchEdgeFixture[]; + stats: Record; + bilalDisplayIdByLocalId: Map; +} + +function portSpec(sourceName: string, slug: string, displayName: string): SpecPortResult { + const sourceDir = resolve(BILAL_ROOT, sourceName, 'graph'); + const nodes = JSON.parse(readFileSync(resolve(sourceDir, 'nodes.json'), 'utf8')) as BilalNode[]; + const edges = JSON.parse(readFileSync(resolve(sourceDir, 'edges.json'), 'utf8')) as BilalEdge[]; + + // Index nodes by bilal uuid for fast lookup + const nodeIndex = new Map(); + for (const n of nodes) nodeIndex.set(n.id, n); + + // Phase 1: identify decision clusters and absorbed entities + const { clusters, absorbedAltIds, absorbedEdgeIds } = buildDecisionClusters(nodes, edges); + + // Phase 2: emit brunch nodes, building bilal-uuid → local-id map + const brunchNodes: BrunchNodeFixture[] = []; + const bilalUuidToLocalId = new Map(); + const localKindByLocalId = new Map(); + const bilalDisplayIdByLocalId = new Map(); + let nextLocalId = 1; + + // 2a. one synthetic "code-audit pass" check, parent of all oracle/evidence + let auditCheckLocalId: number | null = null; + const hasEvidence = nodes.some((n) => n.kind === 'content' && n.semanticRole === 'evidence'); + if (hasEvidence) { + auditCheckLocalId = nextLocalId++; + brunchNodes.push({ + local_id: auditCheckLocalId, + plane: 'oracle', + kind: 'check', + title: `${displayName} — code-audit pass`, + body: + `Synthetic parent check representing the manual code-audit pass during which ` + + `evidence nodes were authored. Generated by ` + + `.fixtures/seed-specs/bilal-port/_port-script.ts to give imported evidence ` + + `a structural parent on the oracle plane.`, + basis: 'explicit', + source: 'derived-port-synthetic', + detail: null, + }); + localKindByLocalId.set(auditCheckLocalId, 'check'); + } + + // 2b. content nodes (skip absorbed alternatives) + for (const node of nodes) { + if (node.kind !== 'content') continue; + if (absorbedAltIds.has(node.id)) continue; + + const classification = classifyContentNode(node); + if (!classification) continue; + + const { basis, source: provenanceSource } = projectProvenance(node); + const source = classification.sourceFlag + ? `${classification.sourceFlag} | ${provenanceSource ?? ''}`.trim().replace(/\| $/, '').trim() + : provenanceSource; + + const localId = nextLocalId++; + bilalUuidToLocalId.set(node.id, localId); + localKindByLocalId.set(localId, classification.kind); + bilalDisplayIdByLocalId.set(localId, node.displayId); + + brunchNodes.push({ + local_id: localId, + plane: classification.plane, + kind: classification.kind, + title: classification.title, + body: classification.body, + basis, + source, + detail: classification.detail, + }); + } + + // 2c. decision hubs (collapsed clusters) + for (const cluster of clusters.values()) { + const classification = classifyDecisionCluster(cluster, nodeIndex); + const { basis, source: provenanceSource } = projectProvenance(cluster.hubNode); + + const localId = nextLocalId++; + bilalUuidToLocalId.set(cluster.hubId, localId); + localKindByLocalId.set(localId, classification.kind); + bilalDisplayIdByLocalId.set(localId, cluster.hubNode.displayId); + // Also map all absorbed alternative ids to the decision local id, + // so any external edge referencing an absorbed alternative redirects + // to the parent decision. + for (const altId of [...cluster.selectedAltIds, ...cluster.rejectedAltIds, ...cluster.consideredAltIds]) { + bilalUuidToLocalId.set(altId, localId); + } + + brunchNodes.push({ + local_id: localId, + plane: classification.plane, + kind: classification.kind, + title: classification.title, + body: classification.body, + basis, + source: provenanceSource, + detail: classification.detail, + }); + } + + // 2d. justification hubs (as context) + for (const node of nodes) { + if (node.kind !== 'hub' || node.hubType !== 'justification') continue; + + const classification = classifyJustificationHub(node); + const { basis, source: provenanceSource } = projectProvenance(node); + const source = classification.sourceFlag + ? `${classification.sourceFlag} | ${provenanceSource ?? ''}`.trim().replace(/\| $/, '').trim() + : provenanceSource; + + const localId = nextLocalId++; + bilalUuidToLocalId.set(node.id, localId); + localKindByLocalId.set(localId, classification.kind); + bilalDisplayIdByLocalId.set(localId, node.displayId); + + brunchNodes.push({ + local_id: localId, + plane: classification.plane, + kind: classification.kind, + title: classification.title, + body: classification.body, + basis, + source, + detail: classification.detail, + }); + } + + // Phase 3: emit brunch edges + const brunchEdges: BrunchEdgeFixture[] = []; + const stats = { + nodes_in: nodes.length, + edges_in: edges.length, + nodes_emitted: brunchNodes.length, + edges_emitted: 0, + edges_absorbed: 0, + edges_dropped_self_after_collapse: 0, + edges_dropped_unresolved_endpoint: 0, + }; + + // 3a. synthesize one realization edge per evidence node, from the audit check + if (auditCheckLocalId !== null) { + for (const node of nodes) { + if (node.kind !== 'content' || node.semanticRole !== 'evidence') continue; + const evidenceLocalId = bilalUuidToLocalId.get(node.id); + if (evidenceLocalId === undefined) continue; + brunchEdges.push({ + category: 'realization', + source_local_id: auditCheckLocalId, + target_local_id: evidenceLocalId, + stance: null, + basis: 'explicit', + rationale: null, + }); + } + } + + // 3b. port real edges + for (const edge of edges) { + if (absorbedEdgeIds.has(edge.id)) { + stats.edges_absorbed++; + continue; + } + const sourceLocalId = bilalUuidToLocalId.get(edge.source.nodeId); + const targetLocalId = bilalUuidToLocalId.get(edge.target.nodeId); + if (sourceLocalId === undefined || targetLocalId === undefined) { + stats.edges_dropped_unresolved_endpoint++; + continue; + } + if (sourceLocalId === targetLocalId) { + // Self-edge after decision-cluster collapse — typically a `consequence` + // or `derived_from` edge where the original target was the selected + // alternative of the same decision (now folded into chosen_option). + // Semantically degenerate after flattening; safe to drop. + stats.edges_dropped_self_after_collapse++; + continue; + } + const targetKind = localKindByLocalId.get(targetLocalId) ?? null; + const mapping = mapEdge(edge, targetKind); + if (!mapping) { + stats.edges_absorbed++; + continue; + } + brunchEdges.push({ + category: mapping.category, + source_local_id: sourceLocalId, + target_local_id: targetLocalId, + stance: mapping.stance, + basis: 'explicit', + rationale: edge.rationale, + }); + } + stats.edges_emitted = brunchEdges.length; + + return { slug, brunchNodes, brunchEdges, stats, bilalDisplayIdByLocalId }; +} + +// --------------------------------------------------------------------------- +// Write output +// --------------------------------------------------------------------------- + +function writeSpec(result: SpecPortResult, displayName: string): void { + const outDir = resolve(OUTPUT_ROOT, result.slug); + if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true }); + mkdirSync(outDir, { recursive: true }); + + writeFileSync( + resolve(outDir, 'spec.json'), + JSON.stringify({ slug: result.slug, name: displayName, readiness_grade: 'commitments_ready' }, null, 2) + + '\n', + ); + writeFileSync(resolve(outDir, 'nodes.json'), JSON.stringify(result.brunchNodes, null, 2) + '\n'); + writeFileSync(resolve(outDir, 'edges.json'), JSON.stringify(result.brunchEdges, null, 2) + '\n'); +} + +function writeReadme(results: { slug: string; displayName: string; stats: Record }[]): void { + const lines: string[] = [ + '# `.fixtures/seed-specs/bilal-port/`', + '', + "Ported spec graphs from Bilal's spec-elicitation prototype, transformed", + 'to the brunch graph model. Intended as development seed data — rich,', + 'real spec material to populate a dev SQLite database for UI / agent work.', + '', + 'Not probe-run artifacts; sits alongside `.fixtures/runs/` rather than inside it.', + '', + '## Provenance', + '', + 'Source: `/Users/lunelson/Code/hashintel/bilal-spec-elicitation-proto/spec//graph/`', + '', + 'Generated by [`_port-script.ts`](./_port-script.ts) (co-located in this directory).', + 'Re-runnable; each run wipes and re-emits per-spec subdirectories.', + '', + '## Transformation rules', + '', + 'See the header docstring of the port script for the full mapping rules,', + 'including: decision-hub-and-spoke collapse, justification-hub absorption,', + 'evidence → oracle plane (with one synthetic per-spec `check`),', + '`risk` and `design` → `context` with source flags for curation,', + 'and the `derived_from` → dependency-vs-support rule keyed on target kind.', + '', + 'Curation flags carried in the `source` field:', + '', + '- `derived-risk-or-question` — was Bilal `risk` semanticRole; many are', + ' literally "Open question (Q##): ..." phrased; per the interrogative', + ' normalization rule in `docs/design/GRAPH_MODEL.md`, curate into', + ' `assumption`, `criterion`, or keep as `context`.', + '- `derived-design-statement` — was Bilal `design` semanticRole; lacks', + ' the structural material to prove a real decision/module; curate into', + ' `decision` (if alternatives recoverable from history), or design plane', + ' `module`/`interface` (if it actually names code).', + '- `derived-justification-synthesis` — was a Bilal `hub:justification`;', + ' rationale appended to body. Curate per case.', + '- `derived-port-synthetic` — node minted by the port script itself', + ' (currently only the per-spec audit `check`).', + '', + '## Output layout', + '', + '```', + 'bilal-port/', + '├── README.md # this file (generated)', + '├── _port-script.ts # the porting script itself (re-runnable)', + '├── /', + '│ ├── spec.json # → specs table seed row', + '│ ├── nodes.json # → nodes table seed rows (local_id placeholder for autoincrement)', + '│ └── edges.json # → edges table seed rows (source/target reference local_id)', + '```', + '', + 'Field shape mirrors [`src/db/schema.ts`](../../../src/db/schema.ts) column names', + '(plane, kind, title, body, basis, source, detail).', + 'No LSNs or change-log entries are pre-baked — a seed loader is expected', + 'to wrap inserts in one `commitGraph`-style transaction so the graph clock,', + "change log, and lsn 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 |', + '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', + ]; + 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} |`, + ); + } + lines.push(''); + writeFileSync(resolve(OUTPUT_ROOT, 'README.md'), lines.join('\n')); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + if (!existsSync(BILAL_ROOT)) { + console.error(`Bilal source directory not found at ${BILAL_ROOT}`); + process.exit(1); + } + mkdirSync(OUTPUT_ROOT, { recursive: true }); + + const summaries: { slug: string; displayName: string; stats: Record }[] = []; + for (const spec of SPECS) { + console.log(`Porting ${spec.source} → ${spec.slug}...`); + const result = portSpec(spec.source, spec.slug, spec.displayName); + writeSpec(result, spec.displayName); + summaries.push({ slug: spec.slug, displayName: spec.displayName, stats: result.stats }); + console.log(` ${JSON.stringify(result.stats)}`); + } + writeReadme(summaries); + console.log(`\nDone. Output at ${OUTPUT_ROOT}`); +} + +main(); diff --git a/.fixtures/seed-specs/bilal-port/code-health/edges.json b/.fixtures/seed-specs/bilal-port/code-health/edges.json new file mode 100644 index 00000000..f2e35bff --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/code-health/edges.json @@ -0,0 +1,4162 @@ +[ + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 12, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 22, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 37, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 49, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 58, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 60, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 66, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 86, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 93, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 96, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 100, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 102, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 113, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 115, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 117, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 132, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 150, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 151, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 154, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 156, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 164, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 165, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 169, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 188, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 204, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 214, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 216, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 226, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 236, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 240, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 243, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 246, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 252, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 254, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 10, + "target_local_id": 164, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 47, + "target_local_id": 96, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 258, + "target_local_id": 15, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 170, + "target_local_id": 257, + "stance": null, + "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": 91, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 265, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 147, + "target_local_id": 253, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 255, + "target_local_id": 15, + "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": 167, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 149, + "target_local_id": 65, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 276, + "stance": null, + "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": 267, + "target_local_id": 146, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 81, + "target_local_id": 264, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 161, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 248, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 266, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 108, + "target_local_id": 150, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 255, + "target_local_id": 158, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 265, + "target_local_id": 113, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 68, + "target_local_id": 260, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 265, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 18, + "target_local_id": 96, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 44, + "target_local_id": 130, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 56, + "target_local_id": 28, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 233, + "target_local_id": 26, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 150, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 227, + "target_local_id": 196, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 15, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 193, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 230, + "target_local_id": 132, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 101, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 237, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 122, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 128, + "target_local_id": 266, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 64, + "target_local_id": 171, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 10, + "target_local_id": 100, + "stance": "for", + "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": 179, + "target_local_id": 270, + "stance": null, + "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": 265, + "target_local_id": 87, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 248, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 127, + "target_local_id": 117, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 243, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 174, + "target_local_id": 225, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 109, + "target_local_id": 64, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 195, + "target_local_id": 87, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 162, + "target_local_id": 28, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 4, + "stance": null, + "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, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 174, + "target_local_id": 250, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 5, + "target_local_id": 97, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 259, + "target_local_id": 215, + "stance": null, + "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": 79, + "target_local_id": 277, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 2, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 16, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 10, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 210, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 77, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 106, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 258, + "target_local_id": 214, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 163, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 178, + "target_local_id": 273, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 203, + "target_local_id": 245, + "stance": null, + "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": 244, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 265, + "target_local_id": 245, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 147, + "target_local_id": 74, + "stance": null, + "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": 262, + "target_local_id": 87, + "stance": null, + "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": 177, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 167, + "target_local_id": 88, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 210, + "target_local_id": 51, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 181, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 231, + "target_local_id": 185, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 270, + "target_local_id": 37, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 181, + "target_local_id": 107, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 47, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 184, + "target_local_id": 253, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 63, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 162, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 271, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 116, + "target_local_id": 93, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 263, + "target_local_id": 37, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 110, + "stance": null, + "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": 11, + "target_local_id": 207, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 135, + "target_local_id": 145, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 91, + "target_local_id": 114, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 92, + "target_local_id": 90, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 210, + "target_local_id": 266, + "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": 261, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 71, + "stance": null, + "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": 35, + "target_local_id": 85, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 122, + "target_local_id": 262, + "stance": null, + "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": 242, + "target_local_id": 142, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 191, + "target_local_id": 82, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 39, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 139, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 253, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 201, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 230, + "target_local_id": 256, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 271, + "target_local_id": 100, + "stance": "for", + "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, + "target_local_id": 272, + "stance": null, + "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": 189, + "target_local_id": 185, + "stance": null, + "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, + "target_local_id": 21, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 265, + "target_local_id": 165, + "stance": "for", + "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": 260, + "target_local_id": 173, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 70, + "target_local_id": 155, + "stance": null, + "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, + "target_local_id": 236, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 23, + "target_local_id": 277, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 161, + "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": 168, + "target_local_id": 155, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 128, + "target_local_id": 151, + "stance": "for", + "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, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 259, + "target_local_id": 144, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 181, + "target_local_id": 34, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 18, + "target_local_id": 55, + "stance": null, + "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": 255, + "target_local_id": 252, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 207, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 225, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 112, + "target_local_id": 156, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 39, + "target_local_id": 142, + "stance": null, + "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, + "target_local_id": 77, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 114, + "stance": null, + "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, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 191, + "target_local_id": 50, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 199, + "stance": null, + "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": 271, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 58, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 108, + "target_local_id": 15, + "stance": null, + "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, + "target_local_id": 274, + "stance": null, + "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": 268, + "target_local_id": 185, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 258, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 167, + "target_local_id": 85, + "stance": null, + "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, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 136, + "target_local_id": 107, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 89, + "target_local_id": 270, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 108, + "target_local_id": 243, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 270, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 152, + "stance": null, + "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": 3, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 53, + "target_local_id": 180, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 182, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 241, + "target_local_id": 152, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 122, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 108, + "target_local_id": 86, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 89, + "target_local_id": 75, + "stance": null, + "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, + "target_local_id": 250, + "stance": null, + "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": 257, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 43, + "target_local_id": 255, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 267, + "target_local_id": 196, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 277, + "target_local_id": 187, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 137, + "target_local_id": 58, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 160, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 52, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 137, + "target_local_id": 255, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 94, + "target_local_id": 95, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 270, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 108, + "target_local_id": 252, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 168, + "target_local_id": 255, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 213, + "target_local_id": 199, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 60, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 128, + "target_local_id": 51, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 277, + "target_local_id": 133, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 54, + "target_local_id": 197, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 91, + "target_local_id": 121, + "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": 178, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 41, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 179, + "target_local_id": 75, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 27, + "target_local_id": 126, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 255, + "target_local_id": 155, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 270, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 249, + "target_local_id": 31, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 24, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 212, + "stance": null, + "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": 64, + "target_local_id": 265, + "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": 275, + "target_local_id": 15, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 138, + "target_local_id": 151, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 189, + "target_local_id": 268, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 167, + "target_local_id": 31, + "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": 94, + "target_local_id": 98, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 259, + "target_local_id": 85, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 227, + "target_local_id": 146, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 256, + "target_local_id": 253, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 119, + "target_local_id": 163, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 149, + "target_local_id": 145, + "stance": null, + "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": 259, + "target_local_id": 126, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 82, + "target_local_id": 113, + "stance": "for", + "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, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 189, + "target_local_id": 24, + "stance": null, + "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": 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, + "target_local_id": 264, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 192, + "stance": null, + "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, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 148, + "target_local_id": 266, + "stance": null, + "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": 262, + "target_local_id": 211, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 77, + "target_local_id": 87, + "stance": null, + "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, + "target_local_id": 142, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 209, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 191, + "target_local_id": 113, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 110, + "target_local_id": 165, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 2, + "target_local_id": 30, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 255, + "target_local_id": 74, + "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": 274, + "target_local_id": 112, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 160, + "target_local_id": 164, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 276, + "target_local_id": 248, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 72, + "target_local_id": 14, + "stance": null, + "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": 242, + "target_local_id": 39, + "stance": null, + "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": 44, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 108, + "target_local_id": 188, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 107, + "stance": null, + "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, + "target_local_id": 143, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 3, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 9, + "target_local_id": 263, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 208, + "target_local_id": 275, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 257, + "target_local_id": 32, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 77, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 188, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 5, + "target_local_id": 238, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 43, + "target_local_id": 154, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 126, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 277, + "target_local_id": 103, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 55, + "target_local_id": 73, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 175, + "stance": null, + "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, + "target_local_id": 199, + "stance": null, + "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": 233, + "target_local_id": 193, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 136, + "target_local_id": 163, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 265, + "target_local_id": 50, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 54, + "target_local_id": 29, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 277, + "target_local_id": 273, + "stance": null, + "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": 52, + "target_local_id": 129, + "stance": null, + "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, + "target_local_id": 103, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 258, + "target_local_id": 251, + "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": 274, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 259, + "target_local_id": 204, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 272, + "target_local_id": 59, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 6, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 219, + "target_local_id": 277, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 262, + "target_local_id": 165, + "stance": "for", + "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": 220, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 213, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 177, + "target_local_id": 66, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 139, + "stance": null, + "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, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 81, + "target_local_id": 25, + "stance": null, + "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": 274, + "target_local_id": 57, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 56, + "target_local_id": 55, + "stance": null, + "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, + "target_local_id": 241, + "stance": null, + "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, + "target_local_id": 273, + "stance": null, + "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, + "target_local_id": 60, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 25, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 209, + "target_local_id": 167, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 262, + "stance": null, + "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, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 220, + "target_local_id": 11, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 135, + "target_local_id": 104, + "stance": null, + "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": 128, + "target_local_id": 241, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 223, + "stance": null, + "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": 270, + "target_local_id": 248, + "stance": null, + "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, + "target_local_id": 45, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 208, + "target_local_id": 101, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 84, + "target_local_id": 106, + "stance": null, + "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": 258, + "target_local_id": 15, + "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, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 184, + "target_local_id": 132, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 62, + "target_local_id": 169, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 267, + "target_local_id": 251, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 189, + "target_local_id": 25, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 26, + "stance": null, + "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, + "target_local_id": 73, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 63, + "target_local_id": 122, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 255, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 44, + "target_local_id": 216, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 18, + "target_local_id": 258, + "stance": null, + "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, + "target_local_id": 142, + "stance": null, + "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, + "target_local_id": 100, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 72, + "target_local_id": 71, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 244, + "target_local_id": 205, + "stance": null, + "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, + "target_local_id": 144, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 260, + "target_local_id": 254, + "stance": "for", + "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": 262, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 86, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 205, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 76, + "target_local_id": 69, + "stance": null, + "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, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 257, + "target_local_id": 71, + "stance": null, + "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, + "target_local_id": 160, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 91, + "target_local_id": 40, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 90, + "stance": null, + "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, + "target_local_id": 202, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 211, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 258, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 271, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 11, + "target_local_id": 262, + "stance": null, + "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, + "target_local_id": 31, + "stance": null, + "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": 56, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 249, + "target_local_id": 85, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 116, + "target_local_id": 59, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 111, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 244, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 35, + "target_local_id": 31, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 161, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 265, + "target_local_id": 226, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 50, + "stance": null, + "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, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 182, + "target_local_id": 245, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 112, + "target_local_id": 66, + "stance": "for", + "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, + "target_local_id": 224, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 23, + "target_local_id": 133, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 52, + "target_local_id": 80, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 27, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 68, + "target_local_id": 254, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 154, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 190, + "target_local_id": 139, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 136, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 125, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 53, + "target_local_id": 155, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 79, + "target_local_id": 133, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 273, + "target_local_id": 199, + "stance": null, + "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, + "target_local_id": 130, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 116, + "target_local_id": 115, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 35, + "target_local_id": 249, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 249, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 6, + "target_local_id": 222, + "stance": null, + "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, + "target_local_id": 144, + "stance": null, + "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, + "target_local_id": 151, + "stance": "for", + "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, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 70, + "target_local_id": 253, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 141, + "stance": null, + "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, + "target_local_id": 153, + "stance": null, + "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, + "target_local_id": 235, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 147, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 109, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 119, + "target_local_id": 57, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 91, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 272, + "target_local_id": 93, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 231, + "target_local_id": 206, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 261, + "target_local_id": 175, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 110, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 11, + "target_local_id": 61, + "stance": null, + "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, + "target_local_id": 159, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 208, + "target_local_id": 205, + "stance": null, + "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, + "target_local_id": 259, + "stance": null, + "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, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 8, + "target_local_id": 143, + "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": 265, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 162, + "target_local_id": 96, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 208, + "target_local_id": 22, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 182, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 213, + "target_local_id": 273, + "stance": null, + "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, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 8, + "target_local_id": 75, + "stance": null, + "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, + "target_local_id": 156, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 148, + "target_local_id": 71, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 123, + "target_local_id": 229, + "stance": null, + "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, + "target_local_id": 49, + "stance": "for", + "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, + "target_local_id": 114, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 247, + "target_local_id": 130, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 186, + "target_local_id": 141, + "stance": null, + "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, + "target_local_id": 185, + "stance": null, + "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, + "target_local_id": 206, + "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/seed-specs/bilal-port/code-health/nodes.json b/.fixtures/seed-specs/bilal-port/code-health/nodes.json new file mode 100644 index 00000000..94f03c81 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/code-health/nodes.json @@ -0,0 +1,2962 @@ +[ + { + "local_id": 1, + "plane": "oracle", + "kind": "check", + "title": "Code Health — code-audit pass", + "body": "Synthetic parent check representing the manual code-audit pass during which evidence nodes were authored. Generated by .fixtures/seed-specs/bilal-port/_port-script.ts to give imported evidence a structural parent on the oracle plane.", + "basis": "explicit", + "source": "derived-port-synthetic", + "detail": null + }, + { + "local_id": 2, + "plane": "intent", + "kind": "requirement", + "title": "Stage 2 must compute three configuration spaces with the semantics defined in T20: M_current (satisfies constraints and current baseline, e…", + "body": "Stage 2 must compute three configuration spaces with the semantics defined in T20: M_current (satisfies constraints and current baseline, excluding alternatives requiring locked-baseline revision), M_preview (includes revision-requiring alternatives tagged as preview-only), and M_revision(r) (after an authorized revision set r is applied).", + "basis": "explicit", + "source": "derived [R22]", + "detail": null + }, + { + "local_id": 3, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must verify that after a refinement reconciliation outcome, the unresolved successor impasse has at least one incoming 'refined…", + "body": "A unit test must verify that after a refinement reconciliation outcome, the unresolved successor impasse has at least one incoming 'refined_to' edge, and that the derivation loop's progress measurement counts it as progress on the 'incoming refined_to edges on unresolved impasses' signal.", + "basis": "explicit", + "source": "derived [CR3]", + "detail": null + }, + { + "local_id": 4, + "plane": "intent", + "kind": "term", + "title": "Node state is modeled across three independent axes: lifecycle (candidate/activ…", + "body": null, + "basis": "explicit", + "source": "external [T9]", + "detail": { + "definition": "Node state is modeled across three independent axes: lifecycle (candidate/active/archived), review status (clean/suspect/conditional), and impasse status (open/resolved/superseded for impasse nodes only)." + } + }, + { + "local_id": 5, + "plane": "intent", + "kind": "requirement", + "title": "Conflict resolution during reconciliation must first attempt a deterministic graph traversal computing the minimal set of grounding nodes w…", + "body": "Conflict resolution during reconciliation must first attempt a deterministic graph traversal computing the minimal set of grounding nodes whose removal resolves the conflict; a subagent may be invoked only when the graph lacks sufficient edge structure (missing provenance edges or semantic-rather-than-structural contradiction).", + "basis": "explicit", + "source": "derived [R64]", + "detail": null + }, + { + "local_id": 6, + "plane": "intent", + "kind": "criterion", + "title": "A module test must drive a fan-in fixture with a witnessed source contradiction where some runs picked sides and verify that Stage 1 emits…", + "body": "A module test must drive a fan-in fixture with a witnessed source contradiction where some runs picked sides and verify that Stage 1 emits a genuine impasse for the contradiction BEFORE Stage 2 computes M_current; ordering verified via EventLog event sequence (FanInExtractionCompleted with impasses[] non-empty precedes ConfigSpaceComputed).", + "basis": "explicit", + "source": "derived [CR29]", + "detail": null + }, + { + "local_id": 7, + "plane": "intent", + "kind": "criterion", + "title": "A static dependency check (parsing deno.json import_map / import statements in engine/solver/**) must confirm that the solver imports nothi…", + "body": "A static dependency check (parsing deno.json import_map / import statements in engine/solver/**) must confirm that the solver imports nothing outside the Deno standard library and Effect; no off-the-shelf SAT library (e.g., logic-solver, minisat, kissat) appears as a dependency.", + "basis": "explicit", + "source": "derived [CR33]", + "detail": null + }, + { + "local_id": 8, + "plane": "intent", + "kind": "criterion", + "title": "A test must verify that each call to cowReplace emits a CowReplace event and each call to markSuspectAndPropagate emits a SuspectPropagated…", + "body": "A test must verify that each call to cowReplace emits a CowReplace event and each call to markSuspectAndPropagate emits a SuspectPropagated event with at least the affected node count.", + "basis": "explicit", + "source": "derived [CR19]", + "detail": null + }, + { + "local_id": 9, + "plane": "intent", + "kind": "requirement", + "title": "The CLI must provide a `resume ` command that loads the latest WorkingGraph artifact, identifies the topmost open frame and earlie…", + "body": "The CLI must provide a `resume ` command that loads the latest WorkingGraph artifact, identifies the topmost open frame and earliest open impasse, and re-enters the derivation loop using that frame as parent.", + "basis": "explicit", + "source": "derived [R52]", + "detail": null + }, + { + "local_id": 10, + "plane": "intent", + "kind": "criterion", + "title": "A test must assert that the derivation loop sets nudgingActive=true after exactly 1 clean attempt without progress (matching X42 and the im…", + "body": "A test must assert that the derivation loop sets nudgingActive=true after exactly 1 clean attempt without progress (matching X42 and the implementation), and that PLAN.md's resolved design question #10 documents nudge_after_n=1.", + "basis": "explicit", + "source": "derived [CR10]", + "detail": null + }, + { + "local_id": 11, + "plane": "intent", + "kind": "requirement", + "title": "The axis 'type' field must accept only 'design' or 'repair'; there must be no 'revision' axis type.", + "body": "The axis 'type' field must accept only 'design' or 'repair'; there must be no 'revision' axis type. Revision is modeled as an effect of selecting a particular alternative, not as a property of an axis.", + "basis": "explicit", + "source": "derived [R16]", + "detail": null + }, + { + "local_id": 12, + "plane": "oracle", + "kind": "evidence", + "title": "Of the 34 code health issues, 8 have been fixed and 26 remain open; the full list is tracked in PROBLEMS.md.", + "body": "Of the 34 code health issues, 8 have been fixed and 26 remain open; the full list is tracked in PROBLEMS.md.", + "basis": "explicit", + "source": "external-observed [E4]", + "detail": null + }, + { + "local_id": 13, + "plane": "intent", + "kind": "term", + "title": "Guarded impasses are diagnostic blockers with a trigger condition (guard formul…", + "body": null, + "basis": "explicit", + "source": "external [T18]", + "detail": { + "definition": "Guarded impasses are diagnostic blockers with a trigger condition (guard formula) over the configuration space; they are not hard constraints and not propositions." + } + }, + { + "local_id": 14, + "plane": "intent", + "kind": "constraint", + "title": "Clean room agents (shaping, pinning, defining-done during re-derivation) get file read but not web search or paper read, because web search…", + "body": "Clean room agents (shaping, pinning, defining-done during re-derivation) get file read but not web search or paper read, because web search results could surface content referencing the hidden impasse or old design.", + "basis": "explicit", + "source": "external [C5]", + "detail": null + }, + { + "local_id": 15, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the recommended priority order for addressing open issues is tests (P18–P25) > correctness (P1, P2, P10/P32, P30) >…", + "body": "Stakeholder preference: the recommended priority order for addressing open issues is tests (P18–P25) > correctness (P1, P2, P10/P32, P30) > design (P16) > everything else.", + "basis": "explicit", + "source": "external [X62]", + "detail": null + }, + { + "local_id": 16, + "plane": "intent", + "kind": "context", + "title": "The resolve_directly and sharpen outcomes from user escalation materialize as grounding nodes with authority: stakeholder and epistemicStat…", + "body": "The resolve_directly and sharpen outcomes from user escalation materialize as grounding nodes with authority: stakeholder and epistemicStatus: asserted, mark trigger impasses as resolved, and return grounding_enriched for re-derivation.", + "basis": "explicit", + "source": "technical-observed [X7]", + "detail": null + }, + { + "local_id": 17, + "plane": "intent", + "kind": "criterion", + "title": "A static lint/grep check must confirm that no file under src/engine/** imports the Console module or invokes Console.log / Console.error /…", + "body": "A static lint/grep check must confirm that no file under src/engine/** imports the Console module or invokes Console.log / Console.error / Console.warn / Console.info / Console.debug. The check must run in CI and fail the build on violation.", + "basis": "explicit", + "source": "derived [CR15]", + "detail": null + }, + { + "local_id": 18, + "plane": "intent", + "kind": "criterion", + "title": "An integration-style test using scripted DerivationAgents and InterventionDriver must trigger a reconciliation outcome that produces a refi…", + "body": "An integration-style test using scripted DerivationAgents and InterventionDriver must trigger a reconciliation outcome that produces a refined impasse, and assert that (a) reconciliation.ts populates spawnedImpasseIds with the new impasse node id, (b) the case 'recurse' branch in derivation-loop.ts is executed (verified via spy/event), and (c) runDerivationLoop is invoked recursively with the new impasse id in triggerImpasseIds.", + "basis": "explicit", + "source": "derived [CR1]", + "detail": null + }, + { + "local_id": 19, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: per-run stance toward alternatives is tracked at the finest granularity — per run, per axis, per alternative value,…", + "body": "Stakeholder preference: per-run stance toward alternatives is tracked at the finest granularity — per run, per axis, per alternative value, with stance values of 'supports', 'contradicts', or 'silent'.", + "basis": "explicit", + "source": "stakeholder [X36]", + "detail": null + }, + { + "local_id": 20, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the model has three distinct layers — hard constraints (boolean formulas determining satisfiability), guarded block…", + "body": "Stakeholder preference: the model has three distinct layers — hard constraints (boolean formulas determining satisfiability), guarded blockers/impasses (diagnostics with trigger conditions), and baseline effects (per-alternative authorization requirements). A configuration is activatable only if it satisfies all three.", + "basis": "explicit", + "source": "stakeholder [X18]", + "detail": null + }, + { + "local_id": 21, + "plane": "intent", + "kind": "context", + "title": "The approach for handling the blocking impasse (unsatisfiable M_current) when selecting which constraint to demote is currently undecided.", + "body": "The approach for handling the blocking impasse (unsatisfiable M_current) when selecting which constraint to demote is currently undecided.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK15]", + "detail": null + }, + { + "local_id": 22, + "plane": "oracle", + "kind": "evidence", + "title": "The spec elicitation prototype has a working forward pass.", + "body": "The spec elicitation prototype has a working forward pass.", + "basis": "explicit", + "source": "external-observed [E1]", + "detail": null + }, + { + "local_id": 23, + "plane": "intent", + "kind": "requirement", + "title": "Perspective summaries must be generated by sampling configurations from the solver's enumeration (capped at 200 per space) and running fart…", + "body": "Perspective summaries must be generated by sampling configurations from the solver's enumeration (capped at 200 per space) and running farthest-first / k-medoids over Hamming distance on axis-assignment vectors to pick k=3 representatives per space, with M_current and M_preview sampled separately.", + "basis": "explicit", + "source": "derived [R23]", + "detail": null + }, + { + "local_id": 24, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: blocking impasse nodes participate in provenance and JTMS chains.", + "body": "Stakeholder preference: blocking impasse nodes participate in provenance and JTMS chains.", + "basis": "explicit", + "source": "stakeholder [X35]", + "detail": null + }, + { + "local_id": 25, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: superseded (OUT) nodes are never deleted from the graph; they are retained with a supersededBy edge pointing to the…", + "body": "Stakeholder preference: superseded (OUT) nodes are never deleted from the graph; they are retained with a supersededBy edge pointing to their replacement, preserving the JTMS justification chain so the graph grows monotonically.", + "basis": "explicit", + "source": "stakeholder [X40]", + "detail": null + }, + { + "local_id": 26, + "plane": "intent", + "kind": "term", + "title": "A checkpoint is an immutable snapshot of the spec graph produced when a full re…", + "body": null, + "basis": "explicit", + "source": "external [T11]", + "detail": { + "definition": "A checkpoint is an immutable snapshot of the spec graph produced when a full revision completes (all impasses resolved, spec stable); checkpoints are not created per frame or reconciliation step." + } + }, + { + "local_id": 27, + "plane": "intent", + "kind": "requirement", + "title": "The solver implementation in engine/solver/dpll.ts must depend only on the Deno standard library and Effect; it must not pull in an off-the…", + "body": "The solver implementation in engine/solver/dpll.ts must depend only on the Deno standard library and Effect; it must not pull in an off-the-shelf SAT library.", + "basis": "explicit", + "source": "derived [R21]", + "detail": null + }, + { + "local_id": 28, + "plane": "oracle", + "kind": "evidence", + "title": "P2: When the reconciler proposes disposition: \"refined\", the reconciliation engine marks the original impasse as superseded but never creat…", + "body": "P2: When the reconciler proposes disposition: \"refined\", the reconciliation engine marks the original impasse as superseded but never creates the refined impasse node; the refinedImpasse field is read from the LLM proposal but not consumed, so the refined impasse silently disappears.", + "basis": "explicit", + "source": "technical-observed [E10]", + "detail": null + }, + { + "local_id": 29, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: when two claims conflict with different authorities, the system surfaces the conflict and labels the authorities, b…", + "body": "Stakeholder preference: when two claims conflict with different authorities, the system surfaces the conflict and labels the authorities, but the user always decides — even when there's an apparent priority cascade.", + "basis": "explicit", + "source": "external [X52]", + "detail": null + }, + { + "local_id": 30, + "plane": "intent", + "kind": "term", + "title": "Three configuration spaces are defined: M_current (satisfies constraints and cu…", + "body": null, + "basis": "explicit", + "source": "external [T20]", + "detail": { + "definition": "Three configuration spaces are defined: M_current (satisfies constraints and current baseline, excluding alternatives requiring locked-baseline revision), M_preview (includes revision-requiring alternatives tagged as preview-only), and M_revision(r) (after an authorized revision set is applied)." + } + }, + { + "local_id": 31, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the SAT solver library must either expose constraint explanations natively or the system must reconstruct them.", + "body": "Stakeholder preference: the SAT solver library must either expose constraint explanations natively or the system must reconstruct them.", + "basis": "explicit", + "source": "stakeholder [X60]", + "detail": null + }, + { + "local_id": 32, + "plane": "intent", + "kind": "context", + "title": "Nudging is tracked as a flag on FrameRecord but never affects agent behavior; no negative constraints are injected into the clean room agen…", + "body": "Nudging is tracked as a flag on FrameRecord but never affects agent behavior; no negative constraints are injected into the clean room agent prompt.", + "basis": "explicit", + "source": "derived-risk-or-question | external-observed [RK3]", + "detail": null + }, + { + "local_id": 33, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: grounding claims require citation, and a separate agent must verify their plausibility.", + "body": "Stakeholder preference: grounding claims require citation, and a separate agent must verify their plausibility.", + "basis": "explicit", + "source": "stakeholder [X58]", + "detail": null + }, + { + "local_id": 34, + "plane": "intent", + "kind": "context", + "title": "Open question (Q11): Taint policy must distinguish evidential contamination (content derived from hidden impasse/old design) from workflow…", + "body": "Open question (Q11): Taint policy must distinguish evidential contamination (content derived from hidden impasse/old design) from workflow provenance (node elicited because of an impasse); the latter should not trigger exclusion or targeted grounding enrichment becomes unusable.", + "basis": "explicit", + "source": "derived-risk-or-question | external [RK11]", + "detail": null + }, + { + "local_id": 35, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must construct a small model where axis X has alternatives {a,b,c} and constraints rule out b and c; backbone(model) must retur…", + "body": "A unit test must construct a small model where axis X has alternatives {a,b,c} and constraints rule out b and c; backbone(model) must return for axis X: {forcedValue:'a', blockingClauses:[, ]}. The blocking clauses must be the actual clauses present in the model.", + "basis": "explicit", + "source": "derived [CR32]", + "detail": null + }, + { + "local_id": 36, + "plane": "intent", + "kind": "criterion", + "title": "A grep check must confirm that the symbol FanInExtractionResult does not appear anywhere in src/** (no definition, no import, no re-export)…", + "body": "A grep check must confirm that the symbol FanInExtractionResult does not appear anywhere in src/** (no definition, no import, no re-export); all import sites in src/agents/fan-in.ts, src/engine/derivation-agents.ts, and src/engine/fan-in.ts must reference ConfigurationSpaceExtractionResult instead.", + "basis": "explicit", + "source": "derived [CR25]", + "detail": null + }, + { + "local_id": 37, + "plane": "oracle", + "kind": "evidence", + "title": "Milestones M1 through M3 and most of M4 are complete; M6 (prose agent) and M9 (perspective hub) are also complete; M5 (resume/polish), M7 (…", + "body": "Milestones M1 through M3 and most of M4 are complete; M6 (prose agent) and M9 (perspective hub) are also complete; M5 (resume/polish), M7 (web inspector), and the end-to-end smoke test remain outstanding.", + "basis": "explicit", + "source": "external-observed [E7]", + "detail": null + }, + { + "local_id": 38, + "plane": "intent", + "kind": "term", + "title": "Conditional labels are ATMS-style truth maintenance markers; without them, reco…", + "body": null, + "basis": "explicit", + "source": "external [T4]", + "detail": { + "definition": "Conditional labels are ATMS-style truth maintenance markers; without them, reconciliation cannot distinguish 'derived under known inconsistency' from 'clean derivation'. They are a correctness property of the core loop, not a display feature." + } + }, + { + "local_id": 39, + "plane": "intent", + "kind": "requirement", + "title": "Stage 1 must only emit a hard constraint when accompanied by explicit witnessing evidence (a source contradiction, a dependency requirement…", + "body": "Stage 1 must only emit a hard constraint when accompanied by explicit witnessing evidence (a source contradiction, a dependency requirement, or a grounded rationale from a run); non-cooccurrence of alternatives across N=4-5 fan-out runs alone must NOT be treated as evidence for a hard constraint.", + "basis": "explicit", + "source": "derived [R18]", + "detail": null + }, + { + "local_id": 40, + "plane": "oracle", + "kind": "evidence", + "title": "The codebase currently uses Console.log extensively throughout the engine for logging (fan-in, fan-out, phase-runner, reconciliation, deriv…", + "body": "The codebase currently uses Console.log extensively throughout the engine for logging (fan-in, fan-out, phase-runner, reconciliation, derivation-loop, etc.).", + "basis": "explicit", + "source": "technical-observed [E8]", + "detail": null + }, + { + "local_id": 41, + "plane": "intent", + "kind": "context", + "title": "Open question: whether a blocking impasse (unsatisfiable configuration space) should be a persistent graph node or a transient grouping con…", + "body": "Open question: whether a blocking impasse (unsatisfiable configuration space) should be a persistent graph node or a transient grouping construct depends on whether it has semantic meaning.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK19]", + "detail": null + }, + { + "local_id": 42, + "plane": "intent", + "kind": "requirement", + "title": "PLAN.md's sections describing fan-in, perspectives, and impasses must be rewritten to reflect the feature-model / SAT model, the deletion o…", + "body": "PLAN.md's sections describing fan-in, perspectives, and impasses must be rewritten to reflect the feature-model / SAT model, the deletion of FanInExtractionResult, perspectives as records, and blocking impasses as graph nodes.", + "basis": "explicit", + "source": "derived [R51]", + "detail": null + }, + { + "local_id": 43, + "plane": "intent", + "kind": "requirement", + "title": "The 1702-line m4-engine.test.ts file must be split into focused per-module test files (one per module covered) colocated with the modules t…", + "body": "The 1702-line m4-engine.test.ts file must be split into focused per-module test files (one per module covered) colocated with the modules they test.", + "basis": "explicit", + "source": "derived [R46]", + "detail": null + }, + { + "local_id": 44, + "plane": "intent", + "kind": "requirement", + "title": "The reconciliation engine must invoke solver.revisionImpact whenever an upstream grounding node's review status flips to suspect, and the O…", + "body": "The reconciliation engine must invoke solver.revisionImpact whenever an upstream grounding node's review status flips to suspect, and the OUT (tainted) closure it returns must be passed into the re-derivation flow.", + "basis": "explicit", + "source": "derived [R30]", + "detail": null + }, + { + "local_id": 45, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the plausibility verification agent takes node content and a source span as input and outputs a stance, rationale,…", + "body": "Stakeholder preference: the plausibility verification agent takes node content and a source span as input and outputs a stance, rationale, and optionally lists of supported and unsupported claims.", + "basis": "explicit", + "source": "stakeholder [X26]", + "detail": null + }, + { + "local_id": 46, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the new ConfigurationSpaceExtractionResult schema must have axes, alternatives, per-run stance, witness relations,…", + "body": "Stakeholder preference: the new ConfigurationSpaceExtractionResult schema must have axes, alternatives, per-run stance, witness relations, and candidate repairs as first-class fields.", + "basis": "explicit", + "source": "stakeholder [X24]", + "detail": null + }, + { + "local_id": 47, + "plane": "intent", + "kind": "context", + "title": "In reconciliation.ts, populate spawnedImpasseIds at the same point where the reconciler proposes new child impasses or where the LLM propos…", + "body": "In reconciliation.ts, populate spawnedImpasseIds at the same point where the reconciler proposes new child impasses or where the LLM proposal includes a refinedImpasse: every node id added to the graph as a new Impasse during reconciliation must also be pushed onto the local spawnedImpasseIds array before the outcome tag is computed. This makes the existing 'recurse' outcome branch and the existing case \"recurse\" handler in derivation-loop.ts (which already passes spawnedImpasseIds as triggerImpasseIds to the recursive runDerivationLoop call) reachable for the first time.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D2]", + "detail": null + }, + { + "local_id": 48, + "plane": "intent", + "kind": "requirement", + "title": "No changes may be made to the Effect AI or @kael/ai Routine abstractions in the course of this work; all integrations must be done at the c…", + "body": "No changes may be made to the Effect AI or @kael/ai Routine abstractions in the course of this work; all integrations must be done at the consumer layer.", + "basis": "explicit", + "source": "derived [R56]", + "detail": null + }, + { + "local_id": 49, + "plane": "oracle", + "kind": "evidence", + "title": "P10/P32: FrameRecord.nudgingActive is set by the derivation loop after nudgeAfterN clean attempts, but no agent or engine code reads it and…", + "body": "P10/P32: FrameRecord.nudgingActive is set by the derivation loop after nudgeAfterN clean attempts, but no agent or engine code reads it and no negative constraint is injected into the clean room prompt.", + "basis": "explicit", + "source": "technical-observed [E14]", + "detail": null + }, + { + "local_id": 50, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: three-valued aggregation (supports/contradicts/silent) is required in fan-in; silence is NOT contradiction and must…", + "body": "Stakeholder preference: three-valued aggregation (supports/contradicts/silent) is required in fan-in; silence is NOT contradiction and must not manufacture fake conflicts from omissions.", + "basis": "explicit", + "source": "external [X50]", + "detail": null + }, + { + "local_id": 51, + "plane": "intent", + "kind": "requirement", + "title": "When a NodeIdFromDisplayId decode fails, the failure must propagate as a structured tool result error visible to the LLM on its next turn t…", + "body": "When a NodeIdFromDisplayId decode fails, the failure must propagate as a structured tool result error visible to the LLM on its next turn through the existing Effect AI retry mechanism, so the agent can correct the reference without engine-side custom retry logic.", + "basis": "explicit", + "source": "derived [R3]", + "detail": null + }, + { + "local_id": 52, + "plane": "intent", + "kind": "requirement", + "title": "Every grounding node produced by the targeted grounding sub-agent or grounding-enrichment must have direct exogenous evidential provenance…", + "body": "Every grounding node produced by the targeted grounding sub-agent or grounding-enrichment must have direct exogenous evidential provenance (citation/source span); the assembler's anti-laundering guardrail must reject grounding-phase events that do not carry such provenance.", + "basis": "explicit", + "source": "derived [R59]", + "detail": null + }, + { + "local_id": 53, + "plane": "intent", + "kind": "requirement", + "title": "Impasse triage must remain a deterministic classifier (no LLM call) using the five-step precedence chain: authority conflict > missing prem…", + "body": "Impasse triage must remain a deterministic classifier (no LLM call) using the five-step precedence chain: authority conflict > missing premise > term/ontology mismatch > upstream structural contradiction > endogenous design conflict (default).", + "basis": "explicit", + "source": "derived [R67]", + "detail": null + }, + { + "local_id": 54, + "plane": "intent", + "kind": "requirement", + "title": "When two claims conflict with different authorities, the system must surface the conflict and label the authorities, but the user must alwa…", + "body": "When two claims conflict with different authorities, the system must surface the conflict and label the authorities, but the user must always make the final decision; the engine must not auto-resolve based on an apparent authority cascade.", + "basis": "explicit", + "source": "derived [R68]", + "detail": null + }, + { + "local_id": 55, + "plane": "intent", + "kind": "requirement", + "title": "The derivation loop's progress measurement must consider three signals: (a) incoming refined_to edges on unresolved impasses, (b) resolved…", + "body": "The derivation loop's progress measurement must consider three signals: (a) incoming refined_to edges on unresolved impasses, (b) resolved impasses, and (c) activated nodes. Lack of progress on all three across an iteration must be treated as stagnation.", + "basis": "explicit", + "source": "derived [R69]", + "detail": null + }, + { + "local_id": 56, + "plane": "intent", + "kind": "criterion", + "title": "A unit test of the reconciliation engine must verify that when a reconciler proposal carries disposition='refined' with a refinedImpasse pa…", + "body": "A unit test of the reconciliation engine must verify that when a reconciler proposal carries disposition='refined' with a refinedImpasse payload: (a) a new Impasse hub node is created in the graph with status 'open', (b) a 'refined_to' lineage edge is created from the original impasse to the new one, (c) the original impasse is marked superseded (impasse-status), and (d) the new node id is pushed onto spawnedImpasseIds.", + "basis": "explicit", + "source": "derived [CR2]", + "detail": null + }, + { + "local_id": 57, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: cowReplace and markSuspectAndPropagate must be called for any milestone that exercises backward transitions such as…", + "body": "Stakeholder preference: cowReplace and markSuspectAndPropagate must be called for any milestone that exercises backward transitions such as grounding enrichment after a missing-premise impasse.", + "basis": "explicit", + "source": "stakeholder [X57]", + "detail": null + }, + { + "local_id": 58, + "plane": "oracle", + "kind": "evidence", + "title": "P22: No test verifies that WorkingGraph.fromArtifact(graph.toArtifact()) preserves all graph state (nodes, edges, frames, display ID counte…", + "body": "P22: No test verifies that WorkingGraph.fromArtifact(graph.toArtifact()) preserves all graph state (nodes, edges, frames, display ID counters, semantic keys); the PLAN marks this as tested but no test exists.", + "basis": "explicit", + "source": "technical-observed [E22]", + "detail": null + }, + { + "local_id": 59, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: each derived node has a justifications list used to determine which beliefs lose support during belief revision.", + "body": "Stakeholder preference: each derived node has a justifications list used to determine which beliefs lose support during belief revision.", + "basis": "explicit", + "source": "stakeholder [X29]", + "detail": null + }, + { + "local_id": 60, + "plane": "oracle", + "kind": "evidence", + "title": "P24: engine/perspective-selection.ts is untested despite being fully testable with a scripted intervention driver.", + "body": "P24: engine/perspective-selection.ts is untested despite being fully testable with a scripted intervention driver.", + "basis": "explicit", + "source": "technical-observed [E24]", + "detail": null + }, + { + "local_id": 61, + "plane": "intent", + "kind": "term", + "title": "There is no 'revision' axis type; revision is an effect of selecting a particul…", + "body": null, + "basis": "explicit", + "source": "external [T16]", + "detail": { + "definition": "There is no 'revision' axis type; revision is an effect of selecting a particular alternative, not a property of an axis." + } + }, + { + "local_id": 62, + "plane": "intent", + "kind": "requirement", + "title": "clean-room-resolution.ts (and the perspective-selection consumer) must read the hasRepairSelections and hasRevisionRequirements flags from…", + "body": "clean-room-resolution.ts (and the perspective-selection consumer) must read the hasRepairSelections and hasRevisionRequirements flags from SelectionOutcome and dispatch to the repair re-derivation flow and revision authorization flow respectively; these flags must no longer be computed-but-unused.", + "basis": "explicit", + "source": "derived [R35]", + "detail": null + }, + { + "local_id": 63, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must instantiate ConfigurationSpaceExtractionResult from src/domain/configuration.ts with all required fields populated and ver…", + "body": "A unit test must instantiate ConfigurationSpaceExtractionResult from src/domain/configuration.ts with all required fields populated and verify the schema accepts: axes (id, type∈{design,repair}, cardinality∈{exactly_one,zero_or_one}, label); alternatives (id, axisId, label); perRunStance (runId, axisId, alternativeId, stance∈{supports,contradicts,silent}, optional rationale); witnesses (runId, claimId, sourceSpan); candidateRepairs (contradictionId, alternativeIds, evidenceStrength); impasses (kind, conflictingNodes); hardConstraints (formula, witnessedBy∈{source_contradiction,dependency,grounded_rationale}, citation). Negative tests must reject invalid stance/type/cardinality values.", + "basis": "explicit", + "source": "derived [CR24]", + "detail": null + }, + { + "local_id": 64, + "plane": "intent", + "kind": "requirement", + "title": "Fan-in must be split into two distinct stages with separate file boundaries: Stage 1 LLM extraction in agents/fan-in.ts producing a Configu…", + "body": "Fan-in must be split into two distinct stages with separate file boundaries: Stage 1 LLM extraction in agents/fan-in.ts producing a ConfigurationSpaceExtractionResult, and Stage 2 deterministic solver analysis in engine/solver.ts (and a new engine/config-model.ts) consuming that result. The two stages must be independently invocable.", + "basis": "explicit", + "source": "derived [R10]", + "detail": null + }, + { + "local_id": 65, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a 'partial' plausibility verdict triggers a split or revision request; an 'unsupported' verdict rejects the node.", + "body": "Stakeholder preference: a 'partial' plausibility verdict triggers a split or revision request; an 'unsupported' verdict rejects the node.", + "basis": "explicit", + "source": "stakeholder [X27]", + "detail": null + }, + { + "local_id": 66, + "plane": "oracle", + "kind": "evidence", + "title": "P34: Four public WorkingGraph methods (cowReplace, markSuspectAndPropagate, getSemanticKey, getChildFrames) are defined but have zero calle…", + "body": "P34: Four public WorkingGraph methods (cowReplace, markSuspectAndPropagate, getSemanticKey, getChildFrames) are defined but have zero callers outside the class; COW grounding updates and suspect propagation are described as core mechanisms in the spec but the engine never exercises them.", + "basis": "explicit", + "source": "technical-observed [E31]", + "detail": null + }, + { + "local_id": 67, + "plane": "intent", + "kind": "requirement", + "title": "The implementation must continue to use pure JSON file I/O; it must not introduce a database (e.g., DuckDB), an in-memory cross-spec graph,…", + "body": "The implementation must continue to use pure JSON file I/O; it must not introduce a database (e.g., DuckDB), an in-memory cross-spec graph, or cross-graph retrieval at this stage.", + "basis": "explicit", + "source": "derived [R57]", + "detail": null + }, + { + "local_id": 68, + "plane": "intent", + "kind": "criterion", + "title": "Unit tests of buildBaselineEffects must verify that: (a) when the baseline node is locked, the effect is {commitmentLevel:'locked', require…", + "body": "Unit tests of buildBaselineEffects must verify that: (a) when the baseline node is locked, the effect is {commitmentLevel:'locked', requiresAuthorization:true}; (b) when the baseline node is provisional, the effect is {commitmentLevel:'provisional', requiresAuthorization:false}; (c) the function reads commitmentLevel from the WorkingGraph baseline node, not from a constant. Verified with two graph fixtures (locked and provisional baseline).", + "basis": "explicit", + "source": "derived [CR8]", + "detail": null + }, + { + "local_id": 69, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: plausibility verification uses a three-valued output — supported, partially-supported, or unsupported — each with a…", + "body": "Stakeholder preference: plausibility verification uses a three-valued output — supported, partially-supported, or unsupported — each with a rationale string.", + "basis": "explicit", + "source": "stakeholder [X25]", + "detail": null + }, + { + "local_id": 70, + "plane": "intent", + "kind": "requirement", + "title": "Module-level tests for derivation-loop, reconciliation, fan-in Stage 2, and the repair re-derivation flow must use scripted DerivationAgent…", + "body": "Module-level tests for derivation-loop, reconciliation, fan-in Stage 2, and the repair re-derivation flow must use scripted DerivationAgents and a scripted InterventionDriver (already injectable per E18) so the tests are deterministic and require no LLM calls.", + "basis": "explicit", + "source": "derived [R42]", + "detail": null + }, + { + "local_id": 71, + "plane": "intent", + "kind": "term", + "title": "Clean room re-derivation is a strict information-flow isolation mechanism: for…", + "body": null, + "basis": "explicit", + "source": "external [T2]", + "detail": { + "definition": "Clean room re-derivation is a strict information-flow isolation mechanism: for a given target phase, it returns only active upstream nodes, creates a fresh Chat instance with no prior history, and ensures retry feedback is schema-only." + } + }, + { + "local_id": 72, + "plane": "intent", + "kind": "requirement", + "title": "Clean room agents (shaping, pinning, defining-done during re-derivation) must be configured with file-read tools only; they must NOT have a…", + "body": "Clean room agents (shaping, pinning, defining-done during re-derivation) must be configured with file-read tools only; they must NOT have access to web search or paper read, because such results could surface content referencing the hidden impasse or old design.", + "basis": "explicit", + "source": "derived [R38]", + "detail": null + }, + { + "local_id": 73, + "plane": "intent", + "kind": "context", + "title": "Progress in the derivation loop is measured by impasse refinement (incoming refined_to edges on unresolved impasses), resolved impasses, an…", + "body": "Progress in the derivation loop is measured by impasse refinement (incoming refined_to edges on unresolved impasses), resolved impasses, and activated nodes.", + "basis": "explicit", + "source": "external-observed [X9]", + "detail": null + }, + { + "local_id": 74, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: integration tests for the derivation loop must cover all three impasse types in sequence using VCR-style recorded i…", + "body": "Stakeholder preference: integration tests for the derivation loop must cover all three impasse types in sequence using VCR-style recorded interaction snapshots against OpenRouter.", + "basis": "explicit", + "source": "stakeholder [X61]", + "detail": null + }, + { + "local_id": 75, + "plane": "intent", + "kind": "requirement", + "title": "Engine events must be defined as a closed discriminated-union type at src/engine/events.ts whose variants include at minimum: PhaseEntered,…", + "body": "Engine events must be defined as a closed discriminated-union type at src/engine/events.ts whose variants include at minimum: PhaseEntered, PhaseCompleted, FanOutAttempt, FanInStarted, FanInExtractionCompleted, ConfigSpaceComputed, PerspectiveGenerated, ReconcileOutcome, ImpasseSpawned, ImpasseResolved, NudgeActivated, CowReplace, SuspectPropagated, BlockingImpasseRaised, UserInterventionRequested, UserInterventionResolved. Adding a new event must require adding a new variant to the union (no open string-tag fallback).", + "basis": "explicit", + "source": "derived [R6]", + "detail": null + }, + { + "local_id": 76, + "plane": "intent", + "kind": "requirement", + "title": "The plausibility verification agent must take node content and a source span as input and produce a three-valued output: 'supported', 'part…", + "body": "The plausibility verification agent must take node content and a source span as input and produce a three-valued output: 'supported', 'partially-supported', or 'unsupported', each accompanied by a rationale string and optionally lists of supported and unsupported claims.", + "basis": "explicit", + "source": "derived [R61]", + "detail": null + }, + { + "local_id": 77, + "plane": "intent", + "kind": "requirement", + "title": "Per-run stance must be tracked at per-run × per-axis × per-alternative granularity; a run must be allowed to support one alternative on an…", + "body": "Per-run stance must be tracked at per-run × per-axis × per-alternative granularity; a run must be allowed to support one alternative on an axis while being silent on another alternative on the same axis.", + "basis": "explicit", + "source": "derived [R15]", + "detail": null + }, + { + "local_id": 78, + "plane": "intent", + "kind": "term", + "title": "Specs are modeled as sub-graphs of typed, addressable claims connected by meani…", + "body": null, + "basis": "explicit", + "source": "external [T6]", + "detail": { + "definition": "Specs are modeled as sub-graphs of typed, addressable claims connected by meaningful edges; the memory system is also a sub-graph within a super-graph architecture where all sub-graphs are searchable through a unified retrieval layer." + } + }, + { + "local_id": 79, + "plane": "intent", + "kind": "requirement", + "title": "Each Perspective record must point at a real activatable configuration drawn from the enumerated set, not at an interpolated centroid.", + "body": "Each Perspective record must point at a real activatable configuration drawn from the enumerated set, not at an interpolated centroid.", + "basis": "explicit", + "source": "derived [R24]", + "detail": null + }, + { + "local_id": 80, + "plane": "intent", + "kind": "context", + "title": "Open question (Q10): The targeted grounding sub-agent could launder prior design choices as facts via memory/cross-spec search; grounding n…", + "body": "Open question (Q10): The targeted grounding sub-agent could launder prior design choices as facts via memory/cross-spec search; grounding nodes must have direct exogenous evidential provenance as a guardrail.", + "basis": "explicit", + "source": "derived-risk-or-question | external [RK10]", + "detail": null + }, + { + "local_id": 81, + "plane": "intent", + "kind": "requirement", + "title": "Superseded (OUT) nodes must never be deleted from the graph; they must be retained with a supersededBy edge pointing to their replacement,…", + "body": "Superseded (OUT) nodes must never be deleted from the graph; they must be retained with a supersededBy edge pointing to their replacement, preserving the JTMS justification chain so the graph grows monotonically.", + "basis": "explicit", + "source": "derived [R32]", + "detail": null + }, + { + "local_id": 82, + "plane": "intent", + "kind": "requirement", + "title": "Per-run stance must be carried as a structured field on ConfigurationSpaceExtractionResult with exactly the values 'supports', 'contradicts…", + "body": "Per-run stance must be carried as a structured field on ConfigurationSpaceExtractionResult with exactly the values 'supports', 'contradicts', or 'silent'; three-valued aggregation in fan-in must read this structured field rather than parsing prose, and silence must never be aggregated as contradiction.", + "basis": "explicit", + "source": "derived [R14]", + "detail": null + }, + { + "local_id": 83, + "plane": "intent", + "kind": "criterion", + "title": "A CLI integration test must run a scripted derivation and verify that the human-readable stdout output is produced by the CLI's EventLog su…", + "body": "A CLI integration test must run a scripted derivation and verify that the human-readable stdout output is produced by the CLI's EventLog subscriber (e.g., by replacing the subscriber with a no-op and asserting stdout is empty), confirming that the CLI consumes EventLog events rather than receiving Console.log calls from the engine.", + "basis": "explicit", + "source": "derived [CR18]", + "detail": null + }, + { + "local_id": 84, + "plane": "intent", + "kind": "requirement", + "title": "When the user resolves a blocking impasse by choosing a constraint demotion, the engine must record that choice as a relaxed_to edge from t…", + "body": "When the user resolves a blocking impasse by choosing a constraint demotion, the engine must record that choice as a relaxed_to edge from the BlockingImpasse node to the demoted constraint node, creating an auditable record.", + "basis": "explicit", + "source": "derived [R27]", + "detail": null + }, + { + "local_id": 85, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: when the solver determines an axis has only one valid value across all configurations (a backbone/forced assignment…", + "body": "Stakeholder preference: when the solver determines an axis has only one valid value across all configurations (a backbone/forced assignment), the system must show which constraint rules made the other values impossible.", + "basis": "explicit", + "source": "stakeholder [X59]", + "detail": null + }, + { + "local_id": 86, + "plane": "oracle", + "kind": "evidence", + "title": "P19: engine/assembler.ts has no unit tests; it converts IR events to graph nodes and edges including reference resolution, hub constraint e…", + "body": "P19: engine/assembler.ts has no unit tests; it converts IR events to graph nodes and edges including reference resolution, hub constraint enforcement, and lineage edge creation.", + "basis": "explicit", + "source": "technical-observed [E19]", + "detail": null + }, + { + "local_id": 87, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a run can support one alternative on an axis while being silent on another alternative on the same axis.", + "body": "Stakeholder preference: a run can support one alternative on an axis while being silent on another alternative on the same axis.", + "basis": "explicit", + "source": "stakeholder [X37]", + "detail": null + }, + { + "local_id": 88, + "plane": "intent", + "kind": "term", + "title": "Substrate (backbone) is the set of alternatives that must be selected (or must…", + "body": null, + "basis": "explicit", + "source": "external [T17]", + "detail": { + "definition": "Substrate (backbone) is the set of alternatives that must be selected (or must not be selected) in every configuration in M_current, defined semantically as common consequences rather than by provenance." + } + }, + { + "local_id": 89, + "plane": "intent", + "kind": "criterion", + "title": "A type-level (compile-time) test must verify that the engine event type defined in src/engine/events.ts is a closed discriminated union: em…", + "body": "A type-level (compile-time) test must verify that the engine event type defined in src/engine/events.ts is a closed discriminated union: emitting an event with an unknown _tag must be a TypeScript compile error. Test method: a `// @ts-expect-error` line that attempts to emit an event with a fabricated tag.", + "basis": "explicit", + "source": "derived [CR17]", + "detail": null + }, + { + "local_id": 90, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: repair choices and design choices are fundamentally different — a repair resolves a source contradiction, a design…", + "body": "Stakeholder preference: repair choices and design choices are fundamentally different — a repair resolves a source contradiction, a design choice selects among valid alternatives. The system must not present them as the same kind of preference.", + "basis": "explicit", + "source": "external [X45]", + "detail": null + }, + { + "local_id": 91, + "plane": "intent", + "kind": "requirement", + "title": "No file under src/engine/** (including fan-in.ts, fan-out.ts, phase-runner.ts, reconciliation.ts, derivation-loop.ts, perspective-selection…", + "body": "No file under src/engine/** (including fan-in.ts, fan-out.ts, phase-runner.ts, reconciliation.ts, derivation-loop.ts, perspective-selection.ts, assembler.ts) may import or call Console.log or any other Console method after the migration; this is enforceable as a static lint/grep check.", + "basis": "explicit", + "source": "derived [R4]", + "detail": null + }, + { + "local_id": 92, + "plane": "intent", + "kind": "requirement", + "title": "Design (non-repair) selection must be monotone: it must NOT trigger taint propagation, OUT computation, or re-derivation.", + "body": "Design (non-repair) selection must be monotone: it must NOT trigger taint propagation, OUT computation, or re-derivation. Only repair selection and revision authorization trigger non-monotone updates.", + "basis": "explicit", + "source": "derived [R34]", + "detail": null + }, + { + "local_id": 93, + "plane": "oracle", + "kind": "evidence", + "title": "The justifications structure (JTMS/ATMS-style truth maintenance) already exists in the codebase on ConfigurationModel.", + "body": "The justifications structure (JTMS/ATMS-style truth maintenance) already exists in the codebase on ConfigurationModel.", + "basis": "explicit", + "source": "technical-observed [E35]", + "detail": null + }, + { + "local_id": 94, + "plane": "intent", + "kind": "requirement", + "title": "When a node carries a cached sourceAuthoritySet, triage must always re-traverse to validate the cache against live graph state; if cached a…", + "body": "When a node carries a cached sourceAuthoritySet, triage must always re-traverse to validate the cache against live graph state; if cached and live results diverge (e.g., due to a supersededBy update), the node must be flagged as requiring re-derivation.", + "basis": "explicit", + "source": "derived [R66]", + "detail": null + }, + { + "local_id": 95, + "plane": "intent", + "kind": "context", + "title": "Open question (Q9): Derived nodes show authority: derived, erasing the original authority basis; triage and reconciliation may need sourceA…", + "body": "Open question (Q9): Derived nodes show authority: derived, erasing the original authority basis; triage and reconciliation may need sourceAuthoritySet / sourceEpistemicBasis summary fields to see through derivation chains.", + "basis": "explicit", + "source": "derived-risk-or-question | external [RK9]", + "detail": null + }, + { + "local_id": 96, + "plane": "oracle", + "kind": "evidence", + "title": "P1: spawnedImpasseIds in reconciliation.ts is always initialized as an empty array and nothing ever pushes to it, making the recurse outcom…", + "body": "P1: spawnedImpasseIds in reconciliation.ts is always initialized as an empty array and nothing ever pushes to it, making the recurse outcome condition unreachable and the derivation loop's case \"recurse\" handler dead code.", + "basis": "explicit", + "source": "technical-observed [E9]", + "detail": null + }, + { + "local_id": 97, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: conflict resolution should always attempt a deterministic graph traversal first, computing the minimal set of groun…", + "body": "Stakeholder preference: conflict resolution should always attempt a deterministic graph traversal first, computing the minimal set of grounding nodes whose removal resolves the conflict.", + "basis": "explicit", + "source": "stakeholder [X55]", + "detail": null + }, + { + "local_id": 98, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: sourceAuthoritySet is stored as a cache on a node at creation time for fast reads, but triage always re-traverses t…", + "body": "Stakeholder preference: sourceAuthoritySet is stored as a cache on a node at creation time for fast reads, but triage always re-traverses to validate it; if cached and live results diverge (e.g., due to a supersededBy update), the node is flagged as requiring re-derivation.", + "basis": "explicit", + "source": "stakeholder [X41]", + "detail": null + }, + { + "local_id": 99, + "plane": "intent", + "kind": "context", + "title": "The derivation pipeline has four phases in strict derivational dependency order: grounding < shaping < pinning < defining-done.", + "body": "The derivation pipeline has four phases in strict derivational dependency order: grounding < shaping < pinning < defining-done. Execution is non-linear via backward transitions, but support edges must remain acyclic.", + "basis": "explicit", + "source": "external [X1]", + "detail": null + }, + { + "local_id": 100, + "plane": "oracle", + "kind": "evidence", + "title": "nudgeAfterN defaults to 1 in the current derivation loop implementation.", + "body": "nudgeAfterN defaults to 1 in the current derivation loop implementation.", + "basis": "explicit", + "source": "technical-observed [E36]", + "detail": null + }, + { + "local_id": 101, + "plane": "intent", + "kind": "constraint", + "title": "Existing smoke test artifacts must still validate after changes.", + "body": "Existing smoke test artifacts must still validate after changes.", + "basis": "explicit", + "source": "external [C4]", + "detail": null + }, + { + "local_id": 102, + "plane": "oracle", + "kind": "evidence", + "title": "P11: suggestedRewindPhase from the agent is always ignored; determineRewindPhase always returns one phase down regardless of the agent's hi…", + "body": "P11: suggestedRewindPhase from the agent is always ignored; determineRewindPhase always returns one phase down regardless of the agent's hint.", + "basis": "explicit", + "source": "technical-observed [E15]", + "detail": null + }, + { + "local_id": 103, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: readiness is evaluated per selected bundle via evaluateSelection, not per perspective; a perspective summary carrie…", + "body": "Stakeholder preference: readiness is evaluated per selected bundle via evaluateSelection, not per perspective; a perspective summary carries default-bundle status for display only.", + "basis": "explicit", + "source": "stakeholder [X22]", + "detail": null + }, + { + "local_id": 104, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: constraint verification follows the same unified mechanical-first / subagent-fallback pattern as plausibility verif…", + "body": "Stakeholder preference: constraint verification follows the same unified mechanical-first / subagent-fallback pattern as plausibility verification — most cases handled mechanically, with uncertain cases elevated to a subagent.", + "basis": "explicit", + "source": "stakeholder [X30]", + "detail": null + }, + { + "local_id": 105, + "plane": "intent", + "kind": "context", + "title": "Open question (Q12): Same-authority normative tradeoffs (latency vs cost, privacy vs observability) also require user adjudication but aren…", + "body": "Open question (Q12): Same-authority normative tradeoffs (latency vs cost, privacy vs observability) also require user adjudication but aren't authority conflicts per se; the triage class name may be too narrow.", + "basis": "explicit", + "source": "derived-risk-or-question | external [RK12]", + "detail": null + }, + { + "local_id": 106, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the user's chosen constraint demotion is recorded as an edge from the blocking impasse node to the relaxed constrai…", + "body": "Stakeholder preference: the user's chosen constraint demotion is recorded as an edge from the blocking impasse node to the relaxed constraint, creating an auditable record.", + "basis": "explicit", + "source": "stakeholder [X34]", + "detail": null + }, + { + "local_id": 107, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: suspect status propagates only through identity-preserving lineage edges (equivalent_to, merged_into); it does NOT…", + "body": "Stakeholder preference: suspect status propagates only through identity-preserving lineage edges (equivalent_to, merged_into); it does NOT auto-propagate through depends_on, derived_from, hub edges, motivates, references, or defines.", + "basis": "explicit", + "source": "external [X53]", + "detail": null + }, + { + "local_id": 108, + "plane": "intent", + "kind": "requirement", + "title": "The codebase must include unit tests for each of the following pure-logic components: buildConfigModel in fan-in.ts, assembler.ts (referenc…", + "body": "The codebase must include unit tests for each of the following pure-logic components: buildConfigModel in fan-in.ts, assembler.ts (reference resolution, hub constraint enforcement, lineage edge creation), makeCleanRoomPolicy in fan-out.ts, perspective-selection.ts, domain/invariants.ts validate() (including violating graphs that exercise support-edge acyclicity and phase stratification), the solver primitives (validateModel, enumerateConfigurations, backbone, demotionCandidates), and render/markdown.ts (snapshot-based).", + "basis": "explicit", + "source": "derived [R40]", + "detail": null + }, + { + "local_id": 109, + "plane": "intent", + "kind": "criterion", + "title": "Module tests must verify that fan-in Stage 1 (LLM extraction in agents/fan-in.ts producing ConfigurationSpaceExtractionResult) and Stage 2…", + "body": "Module tests must verify that fan-in Stage 1 (LLM extraction in agents/fan-in.ts producing ConfigurationSpaceExtractionResult) and Stage 2 (deterministic solver analysis in engine/solver.ts + engine/config-model.ts) can be invoked independently: Stage 2 can be called with a fixture ConfigurationSpaceExtractionResult and produce a configuration model deterministically, without invoking Stage 1.", + "basis": "explicit", + "source": "derived [CR22]", + "detail": null + }, + { + "local_id": 110, + "plane": "intent", + "kind": "requirement", + "title": "The previously-used FanInExtractionResult type must be deleted from the codebase with no backward-compatibility shim; all import sites in s…", + "body": "The previously-used FanInExtractionResult type must be deleted from the codebase with no backward-compatibility shim; all import sites in src/agents/fan-in.ts, src/engine/derivation-agents.ts, and src/engine/fan-in.ts must be updated to use ConfigurationSpaceExtractionResult.", + "basis": "explicit", + "source": "derived [R13]", + "detail": null + }, + { + "local_id": 111, + "plane": "intent", + "kind": "criterion", + "title": "A schema-level test must verify that every agent IR field that previously carried a displayId reference (support sets, conditions, lineageF…", + "body": "A schema-level test must verify that every agent IR field that previously carried a displayId reference (support sets, conditions, lineageFrom prior, conflictingInputs, alternatives, selected, rejected, consequences, premises, conclusions) is typed via NodeIdFromDisplayId and not plain string. Verified by inspecting the exported schemas and asserting the brand/type of each id-bearing field.", + "basis": "explicit", + "source": "derived [CR14]", + "detail": null + }, + { + "local_id": 112, + "plane": "intent", + "kind": "context", + "title": "domain/graph.ts defines WorkingGraph.cowReplace(...) and WorkingGraph.markSuspectAndPropagate(...) as public methods, but a repo-wide searc…", + "body": "domain/graph.ts defines WorkingGraph.cowReplace(...) and WorkingGraph.markSuspectAndPropagate(...) as public methods, but a repo-wide search finds no callers outside the class definition itself.", + "basis": "explicit", + "source": "technical-observed [X64]", + "detail": null + }, + { + "local_id": 113, + "plane": "oracle", + "kind": "evidence", + "title": "P13: The fan-in extraction schema has no structured field for per-run stance (supports/contradicts/silent); three-valued aggregation depend…", + "body": "P13: The fan-in extraction schema has no structured field for per-run stance (supports/contradicts/silent); three-valued aggregation depends entirely on prompt compliance rather than structural enforcement.", + "basis": "explicit", + "source": "technical-observed [E16]", + "detail": null + }, + { + "local_id": 114, + "plane": "intent", + "kind": "context", + "title": "Console.log is used throughout the engine for output, coupling the engine to CLI presentation.", + "body": "Console.log is used throughout the engine for output, coupling the engine to CLI presentation.", + "basis": "explicit", + "source": "derived-risk-or-question | external-observed [RK5]", + "detail": null + }, + { + "local_id": 115, + "plane": "oracle", + "kind": "evidence", + "title": "P8: justifications is always set to an empty array in the configuration model, so the solver's revisionImpact function (JTMS-style truth ma…", + "body": "P8: justifications is always set to an empty array in the configuration model, so the solver's revisionImpact function (JTMS-style truth maintenance) has no data to operate on.", + "basis": "explicit", + "source": "technical-observed [E13]", + "detail": null + }, + { + "local_id": 116, + "plane": "intent", + "kind": "requirement", + "title": "assembler.ts must populate the justifications field on every derived node it creates, with one entry per Justification/Decision/Impasse hub…", + "body": "assembler.ts must populate the justifications field on every derived node it creates, with one entry per Justification/Decision/Impasse hub the node is connected to, recording {hubId, premiseIds: [...]} reflecting the actual hub premise edges.", + "basis": "explicit", + "source": "derived [R29]", + "detail": null + }, + { + "local_id": 117, + "plane": "oracle", + "kind": "evidence", + "title": "P28: The artifact layout in PLAN.md does not list graph/reconciliation-records.json but the code writes it.", + "body": "P28: The artifact layout in PLAN.md does not list graph/reconciliation-records.json but the code writes it.", + "basis": "explicit", + "source": "technical-observed [E28]", + "detail": null + }, + { + "local_id": 118, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: replace the impasse-centric cross-run divergence model with a feature-model / SAT-analyzed constraint problem over…", + "body": "Stakeholder preference: replace the impasse-centric cross-run divergence model with a feature-model / SAT-analyzed constraint problem over a structured variable space; perspectives become a presentation layer over this model rather than the primary semantic unit.", + "basis": "explicit", + "source": "stakeholder [X16]", + "detail": null + }, + { + "local_id": 119, + "plane": "intent", + "kind": "criterion", + "title": "An integration test using scripted intervention/grounding-enrichment must verify that when a stakeholder resolve_directly or sharpen outcom…", + "body": "An integration test using scripted intervention/grounding-enrichment must verify that when a stakeholder resolve_directly or sharpen outcome refines an existing grounding node, WorkingGraph.cowReplace is invoked with {oldNodeId, newNode} and a lineage edge is emitted recording the replacement; verified by spying cowReplace and asserting at least one call per scenario.", + "basis": "explicit", + "source": "derived [CR5]", + "detail": null + }, + { + "local_id": 120, + "plane": "intent", + "kind": "term", + "title": "Grounding is the exogenous substrate: not clean-roomed, using copy-on-write sem…", + "body": null, + "basis": "explicit", + "source": "external [T10]", + "detail": { + "definition": "Grounding is the exogenous substrate: not clean-roomed, using copy-on-write semantics. New nodes are added and existing nodes can be modified via COW. The substrate persists across backward transitions." + } + }, + { + "local_id": 121, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: replace the logging system with the Effect EventLog so every action taken emits an event, replacing all Console.log…", + "body": "Stakeholder preference: replace the logging system with the Effect EventLog so every action taken emits an event, replacing all Console.log calls completely.", + "basis": "explicit", + "source": "stakeholder [X14]", + "detail": null + }, + { + "local_id": 122, + "plane": "intent", + "kind": "requirement", + "title": "ConfigurationSpaceExtractionResult must be defined in src/domain/configuration.ts with first-class fields for: axes (id, type ∈ {design, re…", + "body": "ConfigurationSpaceExtractionResult must be defined in src/domain/configuration.ts with first-class fields for: axes (id, type ∈ {design, repair}, cardinality ∈ {exactly_one, zero_or_one}, label); alternatives (id, axisId, label); perRunStance (runId, axisId, alternativeId, stance ∈ {supports, contradicts, silent}, optional rationale); witnesses (runId, claimId, sourceSpan); candidateRepairs (contradictionId, alternativeIds, evidenceStrength); impasses (kind, conflictingNodes); hardConstraints (formula, witnessedBy ∈ {source_contradiction, dependency, grounded_rationale}, citation).", + "basis": "explicit", + "source": "derived [R12]", + "detail": null + }, + { + "local_id": 123, + "plane": "intent", + "kind": "requirement", + "title": "The implementation must not add support for parallel/concurrent spec design sessions; the architecture remains single-session for this work.", + "body": "The implementation must not add support for parallel/concurrent spec design sessions; the architecture remains single-session for this work.", + "basis": "explicit", + "source": "derived [R58]", + "detail": null + }, + { + "local_id": 124, + "plane": "intent", + "kind": "context", + "title": "Open question (Q8): Impasse triage may need to inspect provenance closure (the 'conflict core') rather than just surface metadata for mixed…", + "body": "Open question (Q8): Impasse triage may need to inspect provenance closure (the 'conflict core') rather than just surface metadata for mixed-cause impasses; missing premise is an absence not visible in node metadata.", + "basis": "explicit", + "source": "derived-risk-or-question | external [RK8]", + "detail": null + }, + { + "local_id": 125, + "plane": "intent", + "kind": "term", + "title": "A run that resolves a source contradiction by picking a side witnesses a candid…", + "body": null, + "basis": "explicit", + "source": "external [T14]", + "detail": { + "definition": "A run that resolves a source contradiction by picking a side witnesses a candidate repair (a possible maximal consistent subset), not an auto-resolution; the contradiction remains until a repair is explicitly licensed." + } + }, + { + "local_id": 126, + "plane": "intent", + "kind": "context", + "title": "Using an off-the-shelf SAT solver risks less control over explanation/proof output and may have Deno compatibility issues.", + "body": "Using an off-the-shelf SAT solver risks less control over explanation/proof output and may have Deno compatibility issues.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK13]", + "detail": null + }, + { + "local_id": 127, + "plane": "intent", + "kind": "requirement", + "title": "PLAN.md's artifact layout section must list graph/reconciliation-records.json so the documented layout matches what the code writes.", + "body": "PLAN.md's artifact layout section must list graph/reconciliation-records.json so the documented layout matches what the code writes.", + "basis": "explicit", + "source": "derived [R49]", + "detail": null + }, + { + "local_id": 128, + "plane": "intent", + "kind": "criterion", + "title": "A module test with a scripted agent that emits an unresolvable display ID must verify the schema decode failure becomes an Effect AI tool r…", + "body": "A module test with a scripted agent that emits an unresolvable display ID must verify the schema decode failure becomes an Effect AI tool result error visible to the LLM on its next turn (i.e., the agent receives a structured retry prompt), and that the engine does not silently filter or drop the reference.", + "basis": "explicit", + "source": "derived [CR12]", + "detail": null + }, + { + "local_id": 129, + "plane": "intent", + "kind": "context", + "title": "The grounding enrichment agent is impasse-aware, uses FullToolkit for research, is constrained to grounding-phase semantic roles only, and…", + "body": "The grounding enrichment agent is impasse-aware, uses FullToolkit for research, is constrained to grounding-phase semantic roles only, and includes an anti-laundering guardrail that validates all enrichment events against the grounding roles set before assembly.", + "basis": "explicit", + "source": "external-observed [X6]", + "detail": null + }, + { + "local_id": 130, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: belief revision uses JTMS-style propagation — a derived node becomes OUT (tainted) when all of its justifications h…", + "body": "Stakeholder preference: belief revision uses JTMS-style propagation — a derived node becomes OUT (tainted) when all of its justifications have at least one IN premise that is suspect.", + "basis": "explicit", + "source": "stakeholder [X28]", + "detail": null + }, + { + "local_id": 131, + "plane": "intent", + "kind": "term", + "title": "The derivation loop is the core post-forward-pass mechanism: it checks for impa…", + "body": null, + "basis": "explicit", + "source": "external [T1]", + "detail": { + "definition": "The derivation loop is the core post-forward-pass mechanism: it checks for impasses, initiates backward transitions by creating child frames, runs clean-room fan-out, reconciles, and recurses inside-out by generation." + } + }, + { + "local_id": 132, + "plane": "oracle", + "kind": "evidence", + "title": "P27: cli/run.ts inlines report formatting (40-line formatHandoffReport and 30-line derivation agent construction) that could be extracted t…", + "body": "P27: cli/run.ts inlines report formatting (40-line formatHandoffReport and 30-line derivation agent construction) that could be extracted to separate modules.", + "basis": "explicit", + "source": "technical-observed [E27]", + "detail": null + }, + { + "local_id": 133, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: perspective summaries are generated from representative configurations using diverse exemplar selection (farthest-f…", + "body": "Stakeholder preference: perspective summaries are generated from representative configurations using diverse exemplar selection (farthest-first / k-medoids over Hamming distance on axis assignments), sampling M_current and M_preview separately.", + "basis": "explicit", + "source": "stakeholder [X21]", + "detail": null + }, + { + "local_id": 134, + "plane": "intent", + "kind": "requirement", + "title": "Reconciliation must not archive a node merely because a re-derivation omitted it; if upstream grounding still supports the node and there i…", + "body": "Reconciliation must not archive a node merely because a re-derivation omitted it; if upstream grounding still supports the node and there is no contradiction, omission is insufficient justification for archival.", + "basis": "explicit", + "source": "derived [R65]", + "detail": null + }, + { + "local_id": 135, + "plane": "intent", + "kind": "requirement", + "title": "Constraint verification must follow the same unified mechanical-first / subagent-fallback pattern as plausibility verification: most cases…", + "body": "Constraint verification must follow the same unified mechanical-first / subagent-fallback pattern as plausibility verification: most cases handled mechanically, with uncertain cases elevated to a subagent. Partial verdicts from the subagent must be fed back to the originating agent for correction.", + "basis": "explicit", + "source": "derived [R63]", + "detail": null + }, + { + "local_id": 136, + "plane": "intent", + "kind": "criterion", + "title": "An integration test must verify that immediately after every cowReplace call, markSuspectAndPropagate(oldNodeId) is invoked and traverses i…", + "body": "An integration test must verify that immediately after every cowReplace call, markSuspectAndPropagate(oldNodeId) is invoked and traverses identity-preserving lineage edges only (equivalent_to, merged_into), setting review status to 'suspect' on transitively reachable nodes; the test must include nodes connected via depends_on / derived_from / hub edges / motivates / references / defines and assert those are NOT marked suspect.", + "basis": "explicit", + "source": "derived [CR6]", + "detail": null + }, + { + "local_id": 137, + "plane": "intent", + "kind": "requirement", + "title": "There must be a unit test that asserts WorkingGraph.fromArtifact(graph.toArtifact()) preserves all graph state, including nodes, edges, fra…", + "body": "There must be a unit test that asserts WorkingGraph.fromArtifact(graph.toArtifact()) preserves all graph state, including nodes, edges, frames, display ID counters, and semantic keys.", + "basis": "explicit", + "source": "derived [R41]", + "detail": null + }, + { + "local_id": 138, + "plane": "intent", + "kind": "criterion", + "title": "A static grep check must confirm that src/engine/assembler.ts contains no '.filter(' expression that drops references whose display ID fail…", + "body": "A static grep check must confirm that src/engine/assembler.ts contains no '.filter(' expression that drops references whose display ID failed to resolve, and that the post-hoc resolve-and-filter code path described in P30 has been removed.", + "basis": "explicit", + "source": "derived [CR13]", + "detail": null + }, + { + "local_id": 139, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a NodeIdFromDisplayId schema type using SchemaGetter.checkEffect should replace all post-hoc display ID resolution,…", + "body": "Stakeholder preference: a NodeIdFromDisplayId schema type using SchemaGetter.checkEffect should replace all post-hoc display ID resolution, so schema decode failures surface as tool result errors that the LLM sees and can retry.", + "basis": "explicit", + "source": "stakeholder [X12]", + "detail": null + }, + { + "local_id": 140, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: enrichment nodes carry dual provenance — workflow provenance (which impasse prompted the inquiry, tracked via motiv…", + "body": "Stakeholder preference: enrichment nodes carry dual provenance — workflow provenance (which impasse prompted the inquiry, tracked via motivates edges) and evidential provenance (the direct exogenous source). Only evidential provenance may justify downstream derivation.", + "basis": "explicit", + "source": "external [X51]", + "detail": null + }, + { + "local_id": 141, + "plane": "intent", + "kind": "context", + "title": "Agents emit semantic keys and support sets during derivation; the graph assembler assigns UUIDs and display IDs and creates edges, keeping…", + "body": "Agents emit semantic keys and support sets during derivation; the graph assembler assigns UUIDs and display IDs and creates edges, keeping normalization deterministic and testable and preventing the model from hallucinating edge targets.", + "basis": "explicit", + "source": "external [X10]", + "detail": null + }, + { + "local_id": 142, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: non-cooccurrence of alternatives across N=4–5 fan-out runs is NOT evidence of a constraint; hard constraints requir…", + "body": "Stakeholder preference: non-cooccurrence of alternatives across N=4–5 fan-out runs is NOT evidence of a constraint; hard constraints require explicit evidence such as a source contradiction, dependency requirement, or grounded rationale from a run.", + "basis": "explicit", + "source": "external [X47]", + "detail": null + }, + { + "local_id": 143, + "plane": "intent", + "kind": "requirement", + "title": "Every notable engine occurrence (phase entry/completion, fan-out attempts, fan-in stages, reconcile outcomes, impasse spawn/resolution, nud…", + "body": "Every notable engine occurrence (phase entry/completion, fan-out attempts, fan-in stages, reconcile outcomes, impasse spawn/resolution, nudge activation, cowReplace, suspect propagation, blocking impasse raise, user intervention request/resolution) must emit a typed event via Effect EventLog.", + "basis": "explicit", + "source": "derived [R5]", + "detail": null + }, + { + "local_id": 144, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: when M_current is unsatisfiable, the system proposes a set of constraint demotions, identifying which demotions wou…", + "body": "Stakeholder preference: when M_current is unsatisfiable, the system proposes a set of constraint demotions, identifying which demotions would make it solvable and which would not.", + "basis": "explicit", + "source": "stakeholder [X32]", + "detail": null + }, + { + "local_id": 145, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: when a partial verdict is returned by the subagent for constraint or plausibility checking, it is fed back to the o…", + "body": "Stakeholder preference: when a partial verdict is returned by the subagent for constraint or plausibility checking, it is fed back to the originating agent for correction.", + "basis": "explicit", + "source": "stakeholder [X31]", + "detail": null + }, + { + "local_id": 146, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: repair selection auto-resolves when one repair option is clearly better-evidenced; only genuinely ambiguous repairs…", + "body": "Stakeholder preference: repair selection auto-resolves when one repair option is clearly better-evidenced; only genuinely ambiguous repairs become user-facing axes.", + "basis": "explicit", + "source": "stakeholder [X43]", + "detail": null + }, + { + "local_id": 147, + "plane": "intent", + "kind": "requirement", + "title": "There must be exactly one end-to-end smoke test that drives the derivation loop through all three impasse types in sequence (authority conf…", + "body": "There must be exactly one end-to-end smoke test that drives the derivation loop through all three impasse types in sequence (authority conflict, missing premise, endogenous design conflict) using VCR-style recorded OpenRouter interaction snapshots.", + "basis": "explicit", + "source": "derived [R44]", + "detail": null + }, + { + "local_id": 148, + "plane": "intent", + "kind": "requirement", + "title": "Each clean-room re-derivation invocation must instantiate a fresh Chat with no prior history; retry feedback to the agent must be schema-on…", + "body": "Each clean-room re-derivation invocation must instantiate a fresh Chat with no prior history; retry feedback to the agent must be schema-only (e.g., NodeIdFromDisplayId decode errors), never freeform.", + "basis": "explicit", + "source": "derived [R39]", + "detail": null + }, + { + "local_id": 149, + "plane": "intent", + "kind": "requirement", + "title": "A 'partially-supported' plausibility verdict must trigger a split-or-revision request fed back to the originating agent for correction; an…", + "body": "A 'partially-supported' plausibility verdict must trigger a split-or-revision request fed back to the originating agent for correction; an 'unsupported' verdict must reject the node.", + "basis": "explicit", + "source": "derived [R62]", + "detail": null + }, + { + "local_id": 150, + "plane": "oracle", + "kind": "evidence", + "title": "P21: buildConfigModel in fan-in.ts, which converts FanInExtractionResult to a typed ConfigurationModel, is untested.", + "body": "P21: buildConfigModel in fan-in.ts, which converts FanInExtractionResult to a typed ConfigurationModel, is untested.", + "basis": "explicit", + "source": "technical-observed [E21]", + "detail": null + }, + { + "local_id": 151, + "plane": "oracle", + "kind": "evidence", + "title": "P30: Display IDs are resolved post-hoc with silent data loss: when a display ID doesn't resolve, the code silently drops it via .filter(nod…", + "body": "P30: Display IDs are resolved post-hoc with silent data loss: when a display ID doesn't resolve, the code silently drops it via .filter(nodeId !== undefined) or logs a non-blocking error, so the LLM never learns its reference was invalid and no retry is triggered.", + "basis": "explicit", + "source": "technical-observed [E30]", + "detail": null + }, + { + "local_id": 152, + "plane": "intent", + "kind": "context", + "title": "Agents produce display IDs as strings; when these don't resolve, data is silently dropped instead of being fed back as errors.", + "body": "Agents produce display IDs as strings; when these don't resolve, data is silently dropped instead of being fed back as errors.", + "basis": "explicit", + "source": "derived-risk-or-question | external-observed [RK2]", + "detail": null + }, + { + "local_id": 153, + "plane": "intent", + "kind": "context", + "title": "The prototype uses pure JSON file I/O; there is no DuckDB, no memory graph, and no cross-graph retrieval.", + "body": "The prototype uses pure JSON file I/O; there is no DuckDB, no memory graph, and no cross-graph retrieval.", + "basis": "explicit", + "source": "external-observed [X3]", + "detail": null + }, + { + "local_id": 154, + "plane": "oracle", + "kind": "evidence", + "title": "P26: m4-engine.test.ts is 1702 lines covering 7 modules and should be split into focused per-module test files.", + "body": "P26: m4-engine.test.ts is 1702 lines covering 7 modules and should be split into focused per-module test files.", + "basis": "explicit", + "source": "technical-observed [E26]", + "detail": null + }, + { + "local_id": 155, + "plane": "intent", + "kind": "constraint", + "title": "Tests must be deterministic and must not make LLM calls.", + "body": "Tests must be deterministic and must not make LLM calls.", + "basis": "explicit", + "source": "external [C2]", + "detail": null + }, + { + "local_id": 156, + "plane": "oracle", + "kind": "evidence", + "title": "cowReplace and markSuspectAndPropagate exist as functions in the graph domain.", + "body": "cowReplace and markSuspectAndPropagate exist as functions in the graph domain.", + "basis": "explicit", + "source": "technical-observed [E34]", + "detail": null + }, + { + "local_id": 157, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: partial selections (user hasn't chosen on some axes yet) are interaction state, not model cardinality; the solver o…", + "body": "Stakeholder preference: partial selections (user hasn't chosen on some axes yet) are interaction state, not model cardinality; the solver operates on total configurations while the UI allows incremental selection.", + "basis": "explicit", + "source": "stakeholder [X17]", + "detail": null + }, + { + "local_id": 158, + "plane": "intent", + "kind": "context", + "title": "VCR-style tests require maintaining recorded snapshots and re-recording when prompts change.", + "body": "VCR-style tests require maintaining recorded snapshots and re-recording when prompts change.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK14]", + "detail": null + }, + { + "local_id": 159, + "plane": "intent", + "kind": "requirement", + "title": "The test suite must include property tests for at least: (a) NodeIdFromDisplayId schema decode round-trips, (b) JTMS revisionImpact monoton…", + "body": "The test suite must include property tests for at least: (a) NodeIdFromDisplayId schema decode round-trips, (b) JTMS revisionImpact monotonicity over repeated mark/unmark, (c) the equality solver.backbone(model) == intersection-of-solver.enumerateConfigurations(model).", + "basis": "explicit", + "source": "derived [R43]", + "detail": null + }, + { + "local_id": 160, + "plane": "intent", + "kind": "requirement", + "title": "PLAN.md's resolved design question #10 must state nudge_after_n default = 1, matching the implementation and X42.", + "body": "PLAN.md's resolved design question #10 must state nudge_after_n default = 1, matching the implementation and X42.", + "basis": "explicit", + "source": "derived [R50]", + "detail": null + }, + { + "local_id": 161, + "plane": "intent", + "kind": "criterion", + "title": "Unit tests of determineRewindPhase must verify: (a) when the agent supplies suggestedRewindPhase strictly upstream of currentPhase, it is h…", + "body": "Unit tests of determineRewindPhase must verify: (a) when the agent supplies suggestedRewindPhase strictly upstream of currentPhase, it is honored (e.g., currentPhase=defining-done + hint=grounding rewinds to grounding); (b) when the hint is absent, it falls back to one phase down; (c) when the hint equals or is downstream of currentPhase, the hint is rejected and the function falls back to one phase down; (d) invalid phase strings are rejected.", + "basis": "explicit", + "source": "derived [CR4]", + "detail": null + }, + { + "local_id": 162, + "plane": "intent", + "kind": "context", + "title": "engine/reconciliation.ts already has the structural plumbing for the 'recurse' outcome (an empty spawnedImpasseIds: NodeId[] array, an outc…", + "body": "engine/reconciliation.ts already has the structural plumbing for the 'recurse' outcome (an empty spawnedImpasseIds: NodeId[] array, an outcomeTag branch, and a return shape with spawnedImpasseIds + suggestedRewindPhase) and engine/derivation-loop.ts already has a case \"recurse\" handler that calls runDerivationLoop with outcome.spawnedImpasseIds; only the population of spawnedImpasseIds is missing.", + "basis": "explicit", + "source": "technical-observed [X63]", + "detail": null + }, + { + "local_id": 163, + "plane": "intent", + "kind": "constraint", + "title": "Wiring cowReplace and markSuspectAndPropagate is a blocking prerequisite: the derivation loop cannot be signed off without it.", + "body": "Wiring cowReplace and markSuspectAndPropagate is a blocking prerequisite: the derivation loop cannot be signed off without it.", + "basis": "explicit", + "source": "stakeholder [C6]", + "detail": null + }, + { + "local_id": 164, + "plane": "oracle", + "kind": "evidence", + "title": "P29: Resolved design question #10 in PLAN.md states nudge_after_n default is 2, but the M8 checklist and derivation loop both use 1; the de…", + "body": "P29: Resolved design question #10 in PLAN.md states nudge_after_n default is 2, but the M8 checklist and derivation loop both use 1; the design question should be updated.", + "basis": "explicit", + "source": "technical-observed [E29]", + "detail": null + }, + { + "local_id": 165, + "plane": "oracle", + "kind": "evidence", + "title": "FanInExtractionResult is currently defined in src/agents/fan-in.ts and re-exported from src/engine/derivation-agents.ts; it is referenced i…", + "body": "FanInExtractionResult is currently defined in src/agents/fan-in.ts and re-exported from src/engine/derivation-agents.ts; it is referenced in src/engine/fan-in.ts.", + "basis": "explicit", + "source": "technical-observed [E37]", + "detail": null + }, + { + "local_id": 166, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: perspectives should be records (like DerivationRunRecord or FanInRecord), not hub nodes in the graph, because they…", + "body": "Stakeholder preference: perspectives should be records (like DerivationRunRecord or FanInRecord), not hub nodes in the graph, because they carry no epistemic weight and nothing downstream derives support through them.", + "basis": "explicit", + "source": "stakeholder [X11]", + "detail": null + }, + { + "local_id": 167, + "plane": "intent", + "kind": "requirement", + "title": "engine/solver/dpll.ts must expose a public surface containing at least: validateModel(model), enumerateConfigurations(model, limit), backbo…", + "body": "engine/solver/dpll.ts must expose a public surface containing at least: validateModel(model), enumerateConfigurations(model, limit), backbone(model) returning per-axis {forcedValue, blockingClauses[]}, and demotionCandidates(model) returning per-constraint {wouldSatisfy: boolean}.", + "basis": "explicit", + "source": "derived [R19]", + "detail": null + }, + { + "local_id": 168, + "plane": "intent", + "kind": "requirement", + "title": "Every test in the unit, module, and property layers must be deterministic and must not make live LLM calls; only the single VCR E2E test ma…", + "body": "Every test in the unit, module, and property layers must be deterministic and must not make live LLM calls; only the single VCR E2E test may interact with OpenRouter, and only via recorded snapshots during normal test execution.", + "basis": "explicit", + "source": "derived [R45]", + "detail": null + }, + { + "local_id": 169, + "plane": "oracle", + "kind": "evidence", + "title": "P7: selectPerspective computes hasRepairSelections and hasRevisionRequirements on SelectionOutcome, but clean-room-resolution.ts never read…", + "body": "P7: selectPerspective computes hasRepairSelections and hasRevisionRequirements on SelectionOutcome, but clean-room-resolution.ts never reads either field; repair selections and revision authorization flows are not implemented.", + "basis": "explicit", + "source": "technical-observed [E12]", + "detail": null + }, + { + "local_id": 170, + "plane": "intent", + "kind": "criterion", + "title": "A unit test of the clean-room prompt builder must verify that when FrameRecord.nudgingActive=true, the assembled prompt contains a negative…", + "body": "A unit test of the clean-room prompt builder must verify that when FrameRecord.nudgingActive=true, the assembled prompt contains a negative-constraint section listing the alternative selections from prior clean attempts in the same frame; when nudgingActive=false, no such section is present. Verified by string-presence assertions on assembled prompt.", + "basis": "explicit", + "source": "derived [CR9]", + "detail": null + }, + { + "local_id": 171, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: split fan-in into two stages — Stage 1 LLM extraction (canonical candidates, contradictions, candidate repairs, axe…", + "body": "Stakeholder preference: split fan-in into two stages — Stage 1 LLM extraction (canonical candidates, contradictions, candidate repairs, axes, alternatives, constraints, impasses, witness relations); Stage 2 deterministic solver analysis (model validation, backbone computation, configuration enumeration, perspective generation).", + "basis": "explicit", + "source": "stakeholder [X19]", + "detail": null + }, + { + "local_id": 172, + "plane": "intent", + "kind": "term", + "title": "Cross-spec references point to specific checkpoints; when a referenced checkpoi…", + "body": null, + "basis": "explicit", + "source": "external [T12]", + "detail": { + "definition": "Cross-spec references point to specific checkpoints; when a referenced checkpoint has a successor, the reference becomes a suspect link for human review rather than silently rebound." + } + }, + { + "local_id": 173, + "plane": "intent", + "kind": "term", + "title": "Frames have both parentFrameId (impasse call stack nesting) and baselineFrameId…", + "body": null, + "basis": "explicit", + "source": "external [T3]", + "detail": { + "definition": "Frames have both parentFrameId (impasse call stack nesting) and baselineFrameId (reconciliation target); in cascading examples these point to different frames." + } + }, + { + "local_id": 174, + "plane": "intent", + "kind": "requirement", + "title": "After a user makes a repair selection on a Perspective record, the engine must perform the following steps in order: (1) mark the un-chosen…", + "body": "After a user makes a repair selection on a Perspective record, the engine must perform the following steps in order: (1) mark the un-chosen-side grounding nodes for the resolved contradiction as suspect; (2) call markSuspectAndPropagate from each; (3) run revisionImpact to compute the OUT set; (4) create a new child frame whose entryPhase is the earliest-affected phase among OUT nodes; (5) re-run the derivation loop in that frame.", + "basis": "explicit", + "source": "derived [R33]", + "detail": null + }, + { + "local_id": 175, + "plane": "intent", + "kind": "context", + "title": "The derivation loop cannot recurse or refine impasses; several reconciliation outcomes are wired in the type system but never produce effec…", + "body": "The derivation loop cannot recurse or refine impasses; several reconciliation outcomes are wired in the type system but never produce effects.", + "basis": "explicit", + "source": "derived-risk-or-question | external-observed [RK1]", + "detail": null + }, + { + "local_id": 176, + "plane": "intent", + "kind": "requirement", + "title": "The resume flow must restart the in-flight frame from its entry phase using the artifact-on-disk graph as the resume substrate; clean-room…", + "body": "The resume flow must restart the in-flight frame from its entry phase using the artifact-on-disk graph as the resume substrate; clean-room re-derivations within that frame must start with a fresh Chat per T2.", + "basis": "explicit", + "source": "derived [R54]", + "detail": null + }, + { + "local_id": 177, + "plane": "intent", + "kind": "criterion", + "title": "A repo-wide grep/static analysis check must confirm that WorkingGraph.cowReplace and WorkingGraph.markSuspectAndPropagate each have at leas…", + "body": "A repo-wide grep/static analysis check must confirm that WorkingGraph.cowReplace and WorkingGraph.markSuspectAndPropagate each have at least one caller outside their defining class (i.e., the methods are no longer orphaned).", + "basis": "explicit", + "source": "derived [CR7]", + "detail": null + }, + { + "local_id": 178, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must verify that perspective summaries are persisted as plain records under graph/ (e.g., attached to FanInRecord/DerivationRun…", + "body": "A unit test must verify that perspective summaries are persisted as plain records under graph/ (e.g., attached to FanInRecord/DerivationRunRecord JSON), each carrying at minimum: id, configuration vector, default-bundle status flag, short label. The test must assert no Perspective hub appears in the graph nodes.", + "basis": "explicit", + "source": "derived [CR21]", + "detail": null + }, + { + "local_id": 179, + "plane": "intent", + "kind": "criterion", + "title": "An integration test driving the engine through a forward pass plus one impasse cycle must subscribe to the Effect EventLog and assert that…", + "body": "An integration test driving the engine through a forward pass plus one impasse cycle must subscribe to the Effect EventLog and assert that at least one event of each of the following tags is observed in the expected order: PhaseEntered, PhaseCompleted, FanOutAttempt, FanInStarted, FanInExtractionCompleted, ConfigSpaceComputed, ReconcileOutcome, ImpasseSpawned, ImpasseResolved, UserInterventionRequested, UserInterventionResolved.", + "basis": "explicit", + "source": "derived [CR16]", + "detail": null + }, + { + "local_id": 180, + "plane": "intent", + "kind": "context", + "title": "Impasse triage is currently a deterministic classifier (no LLM call) with a five-step precedence chain: authority conflict > missing premis…", + "body": "Impasse triage is currently a deterministic classifier (no LLM call) with a five-step precedence chain: authority conflict > missing premise > term/ontology mismatch > upstream structural contradiction > endogenous design conflict (default).", + "basis": "explicit", + "source": "external-observed [X5]", + "detail": null + }, + { + "local_id": 181, + "plane": "intent", + "kind": "requirement", + "title": "Enrichment nodes must carry dual provenance: a workflow provenance edge (motivates) pointing at the impasse that prompted the inquiry, and…", + "body": "Enrichment nodes must carry dual provenance: a workflow provenance edge (motivates) pointing at the impasse that prompted the inquiry, and a separate evidential provenance edge to the direct exogenous source. Only the evidential provenance may be used to justify downstream derivation; workflow provenance must not propagate taint.", + "basis": "explicit", + "source": "derived [R60]", + "detail": null + }, + { + "local_id": 182, + "plane": "intent", + "kind": "criterion", + "title": "A schema/type-level test must verify that ConfigurationSpaceExtractionResult does NOT contain fields representing backbone, mustSelect/must…", + "body": "A schema/type-level test must verify that ConfigurationSpaceExtractionResult does NOT contain fields representing backbone, mustSelect/mustDeselect, enumerated configurations, or scoped-impasse outputs; any attempt to read such a field from the schema must be a TypeScript compile error.", + "basis": "explicit", + "source": "derived [CR23]", + "detail": null + }, + { + "local_id": 183, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the nudge threshold k (nudgeAfterN) is dynamic and set to 1, meaning nudging begins after 1 clean attempt.", + "body": "Stakeholder preference: the nudge threshold k (nudgeAfterN) is dynamic and set to 1, meaning nudging begins after 1 clean attempt.", + "basis": "explicit", + "source": "stakeholder [X42]", + "detail": null + }, + { + "local_id": 184, + "plane": "intent", + "kind": "requirement", + "title": "The 30-line DerivationAgents construction code must be extracted from cli/run.ts into a factory module (engine/derivation-agents-factory.ts…", + "body": "The 30-line DerivationAgents construction code must be extracted from cli/run.ts into a factory module (engine/derivation-agents-factory.ts or src/agents/factory.ts) parameterized by LanguageModel, so tests can inject scripted agents and the CLI can inject the OpenRouter-backed implementation.", + "basis": "explicit", + "source": "derived [R48]", + "detail": null + }, + { + "local_id": 185, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a blocking impasse is a first-class persistent graph node with semantic meaning as a recorded decision point.", + "body": "Stakeholder preference: a blocking impasse is a first-class persistent graph node with semantic meaning as a recorded decision point.", + "basis": "explicit", + "source": "stakeholder [X33]", + "detail": null + }, + { + "local_id": 186, + "plane": "intent", + "kind": "requirement", + "title": "Agents must continue to emit semanticKey and support sets in their IR output; the graph assembler is responsible for assigning UUIDs and di…", + "body": "Agents must continue to emit semanticKey and support sets in their IR output; the graph assembler is responsible for assigning UUIDs and display IDs and creating edges. Agents must not produce UUIDs or display IDs for nodes they are creating in the same batch.", + "basis": "explicit", + "source": "derived [R70]", + "detail": null + }, + { + "local_id": 187, + "plane": "intent", + "kind": "context", + "title": "Open question: how many representative perspective configurations to show (k) and how to handle configuration clustering for M_current vs M…", + "body": "Open question: how many representative perspective configurations to show (k) and how to handle configuration clustering for M_current vs M_preview.", + "basis": "explicit", + "source": "derived-risk-or-question | external-assumed [RK17]", + "detail": null + }, + { + "local_id": 188, + "plane": "oracle", + "kind": "evidence", + "title": "P20: The fan-out conditional label policy (makeCleanRoomPolicy in fan-out.ts) is untested; it is pure logic with no LLM dependency.", + "body": "P20: The fan-out conditional label policy (makeCleanRoomPolicy in fan-out.ts) is untested; it is pure logic with no LLM dependency.", + "basis": "explicit", + "source": "technical-observed [E20]", + "detail": null + }, + { + "local_id": 189, + "plane": "intent", + "kind": "requirement", + "title": "After resolution, the BlockingImpasse node must remain in the graph with status: resolved (not deleted) and must participate in JTMS proven…", + "body": "After resolution, the BlockingImpasse node must remain in the graph with status: resolved (not deleted) and must participate in JTMS provenance chains as a recorded decision point.", + "basis": "explicit", + "source": "derived [R28]", + "detail": null + }, + { + "local_id": 190, + "plane": "intent", + "kind": "requirement", + "title": "Every agent IR field that today carries a displayId reference (support sets, conditions, lineageFrom prior, conflictingInputs, alternatives…", + "body": "Every agent IR field that today carries a displayId reference (support sets, conditions, lineageFrom prior, conflictingInputs, alternatives, selected, rejected, consequences, premises, conclusions, etc.) must be typed via NodeIdFromDisplayId in the agent output schemas instead of plain string.", + "basis": "explicit", + "source": "derived [R1]", + "detail": null + }, + { + "local_id": 191, + "plane": "intent", + "kind": "criterion", + "title": "A unit test of the fan-in aggregation must verify that three-valued aggregation reads stance from the structured perRunStance field (values…", + "body": "A unit test of the fan-in aggregation must verify that three-valued aggregation reads stance from the structured perRunStance field (values exactly 'supports'|'contradicts'|'silent') and not by parsing prose. Negative test: a fixture in which a run is silent on alternative A1 (no perRunStance entry) but supports A2 on the same axis must NOT aggregate as 'contradicts' for A1.", + "basis": "explicit", + "source": "derived [CR26]", + "detail": null + }, + { + "local_id": 192, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: FanInExtractionResult is to be deleted and replaced entirely with a new ConfigurationSpaceExtractionResult schema,…", + "body": "Stakeholder preference: FanInExtractionResult is to be deleted and replaced entirely with a new ConfigurationSpaceExtractionResult schema, with no backward compatibility.", + "basis": "explicit", + "source": "stakeholder [X23]", + "detail": null + }, + { + "local_id": 193, + "plane": "intent", + "kind": "context", + "title": "Session state persistence for the resume command is an open question; the stakeholder expressed preference for no mutable-state checkpoint…", + "body": "Session state persistence for the resume command is an open question; the stakeholder expressed preference for no mutable-state checkpoint dumps and may prefer restarting over complex resumability.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK7]", + "detail": null + }, + { + "local_id": 194, + "plane": "intent", + "kind": "requirement", + "title": "validateModel must reject configuration models in which any axis contains alternatives at mixed abstraction levels (e.g., '2s', '5s', 'conf…", + "body": "validateModel must reject configuration models in which any axis contains alternatives at mixed abstraction levels (e.g., '2s', '5s', 'configurable'); such models must be flagged for manual repair rather than silently accepted.", + "basis": "explicit", + "source": "derived [R37]", + "detail": null + }, + { + "local_id": 195, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must construct a perRunStance fixture in which run R1 supports alternative A1 on axis X and is silent on alternative A2 on the…", + "body": "A unit test must construct a perRunStance fixture in which run R1 supports alternative A1 on axis X and is silent on alternative A2 on the same axis X, and assert that the aggregation accepts and preserves this distinction (no implicit 'all alternatives on the axis share the run's stance').", + "basis": "explicit", + "source": "derived [CR27]", + "detail": null + }, + { + "local_id": 196, + "plane": "intent", + "kind": "context", + "title": "Open question: for v1, consider disabling auto-resolution of repair precedence and surfacing all contradictions as repair axes to preserve…", + "body": "Open question: for v1, consider disabling auto-resolution of repair precedence and surfacing all contradictions as repair axes to preserve correctness at the cost of more user decisions.", + "basis": "explicit", + "source": "derived-risk-or-question | external-assumed [RK18]", + "detail": null + }, + { + "local_id": 197, + "plane": "intent", + "kind": "context", + "title": "Elicitation is interactive by default because the user is the oracle; authority conflicts, unjustified omissions, impasse escalation, and b…", + "body": "Elicitation is interactive by default because the user is the oracle; authority conflicts, unjustified omissions, impasse escalation, and bail decisions cannot be automated.", + "basis": "explicit", + "source": "external [X8]", + "detail": null + }, + { + "local_id": 198, + "plane": "intent", + "kind": "goal", + "title": "The spec elicitation prototype is an AI-assisted system that transforms raw source documents into structured, typed specification graphs th…", + "body": "The spec elicitation prototype is an AI-assisted system that transforms raw source documents into structured, typed specification graphs through a multi-phase derivation pipeline.", + "basis": "explicit", + "source": "external-observed [G1]", + "detail": null + }, + { + "local_id": 199, + "plane": "intent", + "kind": "term", + "title": "Hub nodes (Justification, Decision, Impasse) make joint causation explicit; the…", + "body": null, + "basis": "explicit", + "source": "external [T8]", + "detail": { + "definition": "Hub nodes (Justification, Decision, Impasse) make joint causation explicit; they carry content and connect 1..n incoming edges to 0..n outgoing edges. Decision and Impasse are subtypes of Justification with their own subtype-specific edge roles." + } + }, + { + "local_id": 200, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: genuine impasses must be extracted before the configuration space is computed, to prevent 'some runs picked sides'…", + "body": "Stakeholder preference: genuine impasses must be extracted before the configuration space is computed, to prevent 'some runs picked sides' from hiding a real impasse.", + "basis": "explicit", + "source": "external [X44]", + "detail": null + }, + { + "local_id": 201, + "plane": "intent", + "kind": "requirement", + "title": "Perspective must not appear in the graph's hub-kind union; the hub-kind union remains exactly {Justification, Decision, Impasse} from T8.", + "body": "Perspective must not appear in the graph's hub-kind union; the hub-kind union remains exactly {Justification, Decision, Impasse} from T8. Existing edges that pointed to a Perspective hub must be removed, and any persisted graph artifacts containing Perspective hubs must be migrated or rejected.", + "basis": "explicit", + "source": "derived [R8]", + "detail": null + }, + { + "local_id": 202, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: all alternatives on a single axis must be at the same abstraction level; axes with mixed abstraction levels (e.g.,…", + "body": "Stakeholder preference: all alternatives on a single axis must be at the same abstraction level; axes with mixed abstraction levels (e.g., '2s', '5s', 'configurable') are rejected by validateModel and escalated for manual repair.", + "basis": "explicit", + "source": "external [X46]", + "detail": null + }, + { + "local_id": 203, + "plane": "intent", + "kind": "requirement", + "title": "The Stage 1 ConfigurationSpaceExtractionResult schema must NOT contain fields representing backbone, mustSelect/mustDeselect, enumerated co…", + "body": "The Stage 1 ConfigurationSpaceExtractionResult schema must NOT contain fields representing backbone, mustSelect/mustDeselect, enumerated configurations, or scoped impasses. The schema must structurally forbid the LLM from producing solver outputs.", + "basis": "explicit", + "source": "derived [R11]", + "detail": null + }, + { + "local_id": 204, + "plane": "oracle", + "kind": "evidence", + "title": "The solver already implements backbone computation (mustSelect/mustDeselect) as a deterministic function over the configuration model.", + "body": "The solver already implements backbone computation (mustSelect/mustDeselect) as a deterministic function over the configuration model.", + "basis": "explicit", + "source": "technical-observed [E38]", + "detail": null + }, + { + "local_id": 205, + "plane": "intent", + "kind": "constraint", + "title": "The forward pass must remain working throughout the code health improvements.", + "body": "The forward pass must remain working throughout the code health improvements.", + "basis": "explicit", + "source": "external [C1]", + "detail": null + }, + { + "local_id": 206, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: if M_current is empty, the system is in a dead-end state and must emit a global blocking impasse rather than inferr…", + "body": "Stakeholder preference: if M_current is empty, the system is in a dead-end state and must emit a global blocking impasse rather than inferring substrate from vacuous truth.", + "basis": "explicit", + "source": "external [X48]", + "detail": null + }, + { + "local_id": 207, + "plane": "intent", + "kind": "term", + "title": "An axis is an independent dimension of variability discovered by fan-in; it has…", + "body": null, + "basis": "explicit", + "source": "external [T15]", + "detail": { + "definition": "An axis is an independent dimension of variability discovered by fan-in; it has an id, type (design or repair), cardinality (exactly_one or zero_or_one), and label." + } + }, + { + "local_id": 208, + "plane": "intent", + "kind": "requirement", + "title": "Each of the five staged increments (A correctness wiring, B reference integrity, C observability, D feature-model redesign, E test + hygien…", + "body": "Each of the five staged increments (A correctness wiring, B reference integrity, C observability, D feature-model redesign, E test + hygiene) must be independently mergeable while keeping the forward pass functional and the existing smoke-test artifacts validating.", + "basis": "explicit", + "source": "derived [R55]", + "detail": null + }, + { + "local_id": 209, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must verify the solver module at engine/solver/dpll.ts exports public functions: validateModel(model), enumerateConfigurations(…", + "body": "A unit test must verify the solver module at engine/solver/dpll.ts exports public functions: validateModel(model), enumerateConfigurations(model, limit), backbone(model) returning per-axis {forcedValue, blockingClauses[]}, and demotionCandidates(model) returning per-constraint {wouldSatisfy: boolean}. Calling each with fixture inputs returns the documented shape.", + "basis": "explicit", + "source": "derived [CR31]", + "detail": null + }, + { + "local_id": 210, + "plane": "intent", + "kind": "criterion", + "title": "A unit test must verify that NodeIdFromDisplayId: (a) decodes a valid display ID against a live WorkingGraph to the corresponding NodeId, (…", + "body": "A unit test must verify that NodeIdFromDisplayId: (a) decodes a valid display ID against a live WorkingGraph to the corresponding NodeId, (b) fails decode with a structured error when the display ID does not exist in the graph, (c) round-trips encode/decode for valid IDs (property test).", + "basis": "explicit", + "source": "derived [CR11]", + "detail": null + }, + { + "local_id": 211, + "plane": "intent", + "kind": "term", + "title": "'Silent' stance means a run neither supports nor contradicts a specific alterna…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T19]", + "detail": { + "definition": "'Silent' stance means a run neither supports nor contradicts a specific alternative; it is a distinct value from 'supports' and 'contradicts'." + } + }, + { + "local_id": 212, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: reconciliation cannot archive a node without justification; if upstream grounding still supports a node and there's…", + "body": "Stakeholder preference: reconciliation cannot archive a node without justification; if upstream grounding still supports a node and there's no contradiction, mere omission by re-derivation is insufficient reason to archive.", + "basis": "explicit", + "source": "external [X54]", + "detail": null + }, + { + "local_id": 213, + "plane": "intent", + "kind": "criterion", + "title": "A type-level test must verify that the graph's hub-kind union type is exactly {Justification | Decision | Impasse} and does not include Per…", + "body": "A type-level test must verify that the graph's hub-kind union type is exactly {Justification | Decision | Impasse} and does not include Perspective; an attempt to pattern-match Perspective as a hub kind must fail the TypeScript compiler.", + "basis": "explicit", + "source": "derived [CR20]", + "detail": null + }, + { + "local_id": 214, + "plane": "oracle", + "kind": "evidence", + "title": "The core architecture is sound; problems concentrate in three areas: incomplete wiring in the derivation loop, silent data loss when agents…", + "body": "The core architecture is sound; problems concentrate in three areas: incomplete wiring in the derivation loop, silent data loss when agents produce invalid references, and missing test coverage for pure-logic components.", + "basis": "explicit", + "source": "external-observed [E5]", + "detail": null + }, + { + "local_id": 215, + "plane": "intent", + "kind": "context", + "title": "Open question: should a lightweight existing SAT library or a custom DPLL implementation be used for the solver?", + "body": "Open question: should a lightweight existing SAT library or a custom DPLL implementation be used for the solver? Expected scale (5–20 axes, 2–5 alternatives) is small enough for either.", + "basis": "explicit", + "source": "derived-risk-or-question | external-assumed [RK16]", + "detail": null + }, + { + "local_id": 216, + "plane": "oracle", + "kind": "evidence", + "title": "P17: revisionImpact in solver.ts is implemented but never called outside tests; combined with empty justifications, the entire revision imp…", + "body": "P17: revisionImpact in solver.ts is implemented but never called outside tests; combined with empty justifications, the entire revision impact subsystem is effectively dead.", + "basis": "explicit", + "source": "technical-observed [E17]", + "detail": null + }, + { + "local_id": 217, + "plane": "intent", + "kind": "requirement", + "title": "Perspective summaries must be persisted as plain records attached to FanInRecord (or a sibling DerivationRunRecord) under graph/, each carr…", + "body": "Perspective summaries must be persisted as plain records attached to FanInRecord (or a sibling DerivationRunRecord) under graph/, each carrying at minimum: an id, the configuration vector, a default-bundle status flag (display only), and a short label.", + "basis": "explicit", + "source": "derived [R9]", + "detail": null + }, + { + "local_id": 218, + "plane": "oracle", + "kind": "evidence", + "title": "The derivation loop (which handles impasses, backward transitions, fan-out, and reconciliation) is partially implemented with significant c…", + "body": "The derivation loop (which handles impasses, backward transitions, fan-out, and reconciliation) is partially implemented with significant correctness gaps.", + "basis": "explicit", + "source": "external-observed [E2]", + "detail": null + }, + { + "local_id": 219, + "plane": "intent", + "kind": "requirement", + "title": "Readiness must be evaluated per selected bundle via evaluateSelection; the perspective summary's default-bundle status flag must be display…", + "body": "Readiness must be evaluated per selected bundle via evaluateSelection; the perspective summary's default-bundle status flag must be display-only and must not be used as a readiness gate.", + "basis": "explicit", + "source": "derived [R25]", + "detail": null + }, + { + "local_id": 220, + "plane": "intent", + "kind": "criterion", + "title": "A type-level test must verify that the axis 'type' field accepts only 'design' or 'repair'; constructing an axis with type='revision' must…", + "body": "A type-level test must verify that the axis 'type' field accepts only 'design' or 'repair'; constructing an axis with type='revision' must fail schema decode.", + "basis": "explicit", + "source": "derived [CR28]", + "detail": null + }, + { + "local_id": 221, + "plane": "intent", + "kind": "term", + "title": "The extraction step between raw sources and grounding nodes serves a correctnes…", + "body": null, + "basis": "explicit", + "source": "external [T5]", + "detail": { + "definition": "The extraction step between raw sources and grounding nodes serves a correctness purpose: 'grounding still supports this node' (checked during reconciliation) requires claim-level support, not file-level presence." + } + }, + { + "local_id": 222, + "plane": "intent", + "kind": "requirement", + "title": "Stage 1 fan-in must extract genuine impasses before any configuration space (M_current/M_preview/M_revision) is computed by Stage 2, ensuri…", + "body": "Stage 1 fan-in must extract genuine impasses before any configuration space (M_current/M_preview/M_revision) is computed by Stage 2, ensuring that 'some runs picked a side' on a contradiction cannot mask a real impasse.", + "basis": "explicit", + "source": "derived [R17]", + "detail": null + }, + { + "local_id": 223, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: after repair selection, taint propagation uses markSuspectAndPropagate, and all OUT nodes are re-derived in place v…", + "body": "Stakeholder preference: after repair selection, taint propagation uses markSuspectAndPropagate, and all OUT nodes are re-derived in place via cowReplace by re-running the relevant phase agents.", + "basis": "explicit", + "source": "stakeholder [X38]", + "detail": null + }, + { + "local_id": 224, + "plane": "intent", + "kind": "constraint", + "title": "No changes may be made to the Effect AI or Routine abstractions.", + "body": "No changes may be made to the Effect AI or Routine abstractions.", + "basis": "explicit", + "source": "external [C3]", + "detail": null + }, + { + "local_id": 225, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: re-derivation after repair should enter a new frame and rerun the complete downstream starting from the earliest af…", + "body": "Stakeholder preference: re-derivation after repair should enter a new frame and rerun the complete downstream starting from the earliest affected phase, then reconcile with existing content.", + "basis": "explicit", + "source": "stakeholder [X39]", + "detail": null + }, + { + "local_id": 226, + "plane": "oracle", + "kind": "evidence", + "title": "Fan-out produces N independent derivations of the same phase; when they disagree, the current system creates impasse nodes, which causes an…", + "body": "Fan-out produces N independent derivations of the same phase; when they disagree, the current system creates impasse nodes, which causes an infinite loop because re-derivation cannot resolve inherent design-space disagreements.", + "basis": "explicit", + "source": "external-observed [E32]", + "detail": null + }, + { + "local_id": 227, + "plane": "intent", + "kind": "requirement", + "title": "Solver-side auto-resolution of repair precedence must be disabled by default in v1 via a config.repairAutoResolve flag defaulting to false;…", + "body": "Solver-side auto-resolution of repair precedence must be disabled by default in v1 via a config.repairAutoResolve flag defaulting to false; every detected contradiction must surface as a repair axis to the user regardless of evidence asymmetry. The auto-resolution code path must be feature-flagged rather than removed.", + "basis": "explicit", + "source": "derived [R36]", + "detail": null + }, + { + "local_id": 228, + "plane": "intent", + "kind": "context", + "title": "No integration test exercises the full triage-to-resolution pipeline.", + "body": "No integration test exercises the full triage-to-resolution pipeline.", + "basis": "explicit", + "source": "derived-risk-or-question | external-observed [RK6]", + "detail": null + }, + { + "local_id": 229, + "plane": "intent", + "kind": "context", + "title": "Supporting parallel/concurrent spec design sessions is deferred because each spec design can take 20 minutes to an hour and internal state…", + "body": "Supporting parallel/concurrent spec design sessions is deferred because each spec design can take 20 minutes to an hour and internal state is not centralized, making concurrency incompatible with the current architecture.", + "basis": "explicit", + "source": "stakeholder [X4]", + "detail": null + }, + { + "local_id": 230, + "plane": "intent", + "kind": "requirement", + "title": "The 40-line formatHandoffReport function must be extracted from cli/run.ts into cli/format-handoff-report.ts as a pure function (HandoffRep…", + "body": "The 40-line formatHandoffReport function must be extracted from cli/run.ts into cli/format-handoff-report.ts as a pure function (HandoffReport → string) with snapshot-based unit tests.", + "basis": "explicit", + "source": "derived [R47]", + "detail": null + }, + { + "local_id": 231, + "plane": "intent", + "kind": "requirement", + "title": "When the solver determines M_current is empty or unsatisfiable, the engine must create a first-class Impasse hub node (kind: 'unsatisfiable…", + "body": "When the solver determines M_current is empty or unsatisfiable, the engine must create a first-class Impasse hub node (kind: 'unsatisfiable_configuration_space') in the graph with status: open and support edges to all hard-constraint nodes participating in the UNSAT core; the engine must NOT infer substrate from vacuous truth.", + "basis": "explicit", + "source": "derived [R26]", + "detail": null + }, + { + "local_id": 232, + "plane": "intent", + "kind": "context", + "title": "The tech stack is: Deno runtime, Effect v4 (beta 57), Effect CLI, Effect AI, @kael/ai (Fragment, Routine), @kael/core/platform, and @effect…", + "body": "The tech stack is: Deno runtime, Effect v4 (beta 57), Effect CLI, Effect AI, @kael/ai (Fragment, Routine), @kael/core/platform, and @effect/platform-node-shared. All agents use the LanguageModel abstraction; the concrete provider is OpenRouter wired at the CLI entry point.", + "basis": "explicit", + "source": "external-observed [X2]", + "detail": null + }, + { + "local_id": 233, + "plane": "intent", + "kind": "requirement", + "title": "The system must not write mid-derivation checkpoint dumps (no serialization of WorkingGraph + frame stack + current chat at each reconcilia…", + "body": "The system must not write mid-derivation checkpoint dumps (no serialization of WorkingGraph + frame stack + current chat at each reconciliation step). The only checkpointable state is a completed checkpoint produced when a full revision completes.", + "basis": "explicit", + "source": "derived [R53]", + "detail": null + }, + { + "local_id": 234, + "plane": "intent", + "kind": "term", + "title": "Spec nodes are organized across three orthogonal axes: semantic role (goal, ter…", + "body": null, + "basis": "explicit", + "source": "external [T7]", + "detail": { + "definition": "Spec nodes are organized across three orthogonal axes: semantic role (goal, term, context, constraint, evidence, design, alternative, requirement, criterion, risk), epistemic status (observed, asserted, assumed, inferred), and authority (stakeholder, technical, external, derived)." + } + }, + { + "local_id": 235, + "plane": "intent", + "kind": "context", + "title": "Perspectives are currently modeled as hub nodes alongside justifications, decisions, and impasses, but carry no epistemic weight and should…", + "body": "Perspectives are currently modeled as hub nodes alongside justifications, decisions, and impasses, but carry no epistemic weight and should be records instead.", + "basis": "explicit", + "source": "derived-risk-or-question | external-observed [RK4]", + "detail": null + }, + { + "local_id": 236, + "plane": "oracle", + "kind": "evidence", + "title": "The current impasse-based model for cross-run divergence uses the wrong mental model: it asks the user to resolve individual point-conflict…", + "body": "The current impasse-based model for cross-run divergence uses the wrong mental model: it asks the user to resolve individual point-conflicts when the real question is which overall design vision they want.", + "basis": "explicit", + "source": "external [E33]", + "detail": null + }, + { + "local_id": 237, + "plane": "intent", + "kind": "term", + "title": "Cross-run divergence has three distinct categories: genuine impasse (source pol…", + "body": null, + "basis": "explicit", + "source": "external [T13]", + "detail": { + "definition": "Cross-run divergence has three distinct categories: genuine impasse (source policy conflict), design perspective (both grounded and coherent alternatives), and derivation noise (hallucinated without grounding basis)." + } + }, + { + "local_id": 238, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: a subagent is invoked for conflict resolution only when the graph lacks sufficient edge structure — e.g., when prov…", + "body": "Stakeholder preference: a subagent is invoked for conflict resolution only when the graph lacks sufficient edge structure — e.g., when provenance edges are missing or the contradiction is semantic rather than structural.", + "basis": "explicit", + "source": "stakeholder [X56]", + "detail": null + }, + { + "local_id": 239, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: replace Console.log calls throughout the engine with either Effect's structured logging (Effect.logInfo/Effect.logD…", + "body": "Stakeholder preference: replace Console.log calls throughout the engine with either Effect's structured logging (Effect.logInfo/Effect.logDebug) or a typed event bus with structured per-operation events, so the CLI becomes one consumer among many.", + "basis": "explicit", + "source": "stakeholder [X13]", + "detail": null + }, + { + "local_id": 240, + "plane": "oracle", + "kind": "evidence", + "title": "A code health review of the spec elicitation prototype uncovered 34 issues across correctness, design, testing, and systemic architecture.", + "body": "A code health review of the spec elicitation prototype uncovered 34 issues across correctness, design, testing, and systemic architecture.", + "basis": "explicit", + "source": "external-observed [E3]", + "detail": null + }, + { + "local_id": 241, + "plane": "intent", + "kind": "requirement", + "title": "assembler.ts must not silently drop or filter out unresolved displayId references; the post-hoc resolve-and-filter step (.filter(nodeId !==…", + "body": "assembler.ts must not silently drop or filter out unresolved displayId references; the post-hoc resolve-and-filter step (.filter(nodeId !== undefined) and non-blocking error logging) must be removed in favor of schema-level decode failures that surface as Effect AI tool result errors.", + "basis": "explicit", + "source": "derived [R2]", + "detail": null + }, + { + "local_id": 242, + "plane": "intent", + "kind": "criterion", + "title": "A unit test of Stage 1 extraction must verify that hard constraints are emitted only with witnessedBy ∈ {source_contradiction, dependency,…", + "body": "A unit test of Stage 1 extraction must verify that hard constraints are emitted only with witnessedBy ∈ {source_contradiction, dependency, grounded_rationale} and a citation; negative test: a fixture exhibiting non-cooccurrence of alternatives across 5 fan-out runs without any witnessing rationale must NOT produce a hard constraint.", + "basis": "explicit", + "source": "derived [CR30]", + "detail": null + }, + { + "local_id": 243, + "plane": "oracle", + "kind": "evidence", + "title": "P23: domain/invariants.ts has support-edge acyclicity detection and phase stratification checks, but these are not tested with violating gr…", + "body": "P23: domain/invariants.ts has support-edge acyclicity detection and phase stratification checks, but these are not tested with violating graphs; validate() is not called in any test file.", + "basis": "explicit", + "source": "technical-observed [E23]", + "detail": null + }, + { + "local_id": 244, + "plane": "intent", + "kind": "requirement", + "title": "The CLI (cli/run.ts and cli-driver.ts) must consume engine events as a subscriber to the EventLog rather than receiving them via Console.lo…", + "body": "The CLI (cli/run.ts and cli-driver.ts) must consume engine events as a subscriber to the EventLog rather than receiving them via Console.log; the CLI must continue to render human-readable output equivalent to the prior Console.log output for each event variant it cares about.", + "basis": "explicit", + "source": "derived [R7]", + "detail": null + }, + { + "local_id": 245, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the LLM extraction stage must NOT compute backbone, enumerate configurations, or scope impasses; those are determin…", + "body": "Stakeholder preference: the LLM extraction stage must NOT compute backbone, enumerate configurations, or scope impasses; those are deterministic solver operations.", + "basis": "explicit", + "source": "external [X49]", + "detail": null + }, + { + "local_id": 246, + "plane": "oracle", + "kind": "evidence", + "title": "The system is implemented as a standalone CLI prototype in packages/experimental/spec-elicitation/, containing src/, spec/, PLAN.md, and PR…", + "body": "The system is implemented as a standalone CLI prototype in packages/experimental/spec-elicitation/, containing src/, spec/, PLAN.md, and PROBLEMS.md.", + "basis": "explicit", + "source": "technical-observed [E6]", + "detail": null + }, + { + "local_id": 247, + "plane": "intent", + "kind": "requirement", + "title": "revisionImpact must mark a derived node as OUT (tainted) when all of its justifications have at least one IN premise that is suspect, match…", + "body": "revisionImpact must mark a derived node as OUT (tainted) when all of its justifications have at least one IN premise that is suspect, matching the JTMS-style propagation rule.", + "basis": "explicit", + "source": "derived [R31]", + "detail": null + }, + { + "local_id": 248, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: the EventLog should emit events beyond simple Console.log replacements for notable occurrences; finer granularity i…", + "body": "Stakeholder preference: the EventLog should emit events beyond simple Console.log replacements for notable occurrences; finer granularity is better but not every tiny detail needs an event.", + "basis": "explicit", + "source": "stakeholder [X15]", + "detail": null + }, + { + "local_id": 249, + "plane": "intent", + "kind": "requirement", + "title": "The backbone function must return, for every axis whose value is forced, the set of constraint clauses (blockingClauses) that ruled out the…", + "body": "The backbone function must return, for every axis whose value is forced, the set of constraint clauses (blockingClauses) that ruled out the alternatives that were not forced, so the user can see which constraints made the other values impossible.", + "basis": "explicit", + "source": "derived [R20]", + "detail": null + }, + { + "local_id": 250, + "plane": "intent", + "kind": "context", + "title": "Stakeholder preference: perspective selection by the user is not new evidence; design selection is monotone (no re-derivation needed), repa…", + "body": "Stakeholder preference: perspective selection by the user is not new evidence; design selection is monotone (no re-derivation needed), repair selection is non-monotone with respect to premises (downstream phases must be recomputed), and revision authorization requires the revision flow.", + "basis": "explicit", + "source": "stakeholder [X20]", + "detail": null + }, + { + "local_id": 251, + "plane": "intent", + "kind": "constraint", + "title": "The resolution strategy must be maximally correct and must not take shortcuts.", + "body": "The resolution strategy must be maximally correct and must not take shortcuts.", + "basis": "explicit", + "source": "stakeholder [C7]", + "detail": null + }, + { + "local_id": 252, + "plane": "oracle", + "kind": "evidence", + "title": "P25: render/markdown.ts (329 lines) has no tests; a snapshot test with a known artifact would catch rendering regressions.", + "body": "P25: render/markdown.ts (329 lines) has no tests; a snapshot test with a known artifact would catch rendering regressions.", + "basis": "explicit", + "source": "technical-observed [E25]", + "detail": null + }, + { + "local_id": 253, + "plane": "oracle", + "kind": "evidence", + "title": "P18: No end-to-end smoke test with an impasse scenario exists; DerivationAgents and InterventionDriver are injectable services so a determi…", + "body": "P18: No end-to-end smoke test with an impasse scenario exists; DerivationAgents and InterventionDriver are injectable services so a deterministic integration test is possible.", + "basis": "explicit", + "source": "technical-observed [E18]", + "detail": null + }, + { + "local_id": 254, + "plane": "oracle", + "kind": "evidence", + "title": "P6: All baseline effects in fan-in.ts are hardcoded to commitmentLevel: \"locked\" and requiresAuthorization: true, without checking whether…", + "body": "P6: All baseline effects in fan-in.ts are hardcoded to commitmentLevel: \"locked\" and requiresAuthorization: true, without checking whether the baseline node is actually locked or provisional.", + "basis": "explicit", + "source": "technical-observed [E11]", + "detail": null + }, + { + "local_id": 255, + "plane": "intent", + "kind": "decision", + "title": "Use a four-layer pyramid: unit + module (scripted) + property + one VCR E2E.", + "body": "Use a four-layer pyramid: unit + module (scripted) + property + one VCR E2E.", + "basis": "explicit", + "source": "[DEC19]", + "detail": { + "chosen_option": "Test strategy is a four-layer pyramid: (1) Unit tests for pure logic — buildConfigModel (P21), assembler.ts (P19), makeCleanRoomPolicy (P20), perspective-selection.ts (P24), invariants.ts validate() including violating graphs (P23), solver primitives (validate/enumerate/backbone/demote), markdown render (P25), WorkingGraph artifact roundtrip (P22). (2) Module tests with scripted DerivationAgents and InterventionDriver (already injectable per E18) for derivation-loop, reconciliation, fan-in stage 2, and the repair re-derivation flow. (3) Property tests for: (a) NodeIdFromDisplayId schema decode round-trips, (b) JTMS revisionImpact monotonicity over repeated mark/unmark, (c) solver backbone equals intersection of enumerate(). (4) One end-to-end smoke test (P18) using VCR-recorded OpenRouter interactions covering all three impasse types in sequence (X61). The 1702-line m4-engine.test.ts (P26) is split per module into test files colocated with the modules they cover.", + "rejected": [ + "Alternative: invest only in unit tests for P19–P25 and skip the VCR E2E because of the maintenance burden of recorded snapshots (RK14).", + "Alternative: invest only in the end-to-end VCR test (X61) and rely on it to indirectly cover assembler/solver/etc., skipping per-module unit tests." + ], + "rationale": "X62 puts tests P18–P25 ahead of correctness fixes — a test bar this high cannot rest on either alt-test-only-e2e (which leaves pure-logic regressions like P19–P25 invisible until the slow E2E catches them) or alt-test-only-units (RK6 is unaddressed; nothing exercises triage-to-resolution). C2 (deterministic, no LLM calls) is satisfied by layers 1–3 because they use scripted DerivationAgents (E18 confirms this is possible). The single VCR test bounds the maintenance cost flagged by RK14: re-record only when prompts change, and only one recording to maintain. Property tests are added for the components most likely to silently regress (schema decode, JTMS, backbone) where example-based unit tests provide weak assurance." + } + }, + { + "local_id": 256, + "plane": "intent", + "kind": "decision", + "title": "Extract format-handoff-report and derivation-agents-factory into separate modules.", + "body": "Extract format-handoff-report and derivation-agents-factory into separate modules.", + "basis": "explicit", + "source": "[DEC20]", + "detail": { + "chosen_option": "Extract from cli/run.ts: (a) cli/format-handoff-report.ts containing the 40-line formatHandoffReport function as a pure function over HandoffReport → string, unit-tested with snapshot fixtures; (b) engine/derivation-agents-factory.ts (or src/agents/factory.ts) containing the 30-line DerivationAgents construction wiring, parameterized by LanguageModel so tests inject scripted agents and the CLI injects the OpenRouter-backed one. cli/run.ts becomes a thin orchestrator that imports both.", + "rejected": [ + "Alternative: leave the inlined helpers in cli/run.ts; the prototype is small enough that the extra module hop isn't worth the indirection." + ], + "rationale": "Both helpers are referenced by the test pyramid (formatHandoffReport needs snapshot tests against fixture HandoffReports; the factory is needed by every module test that wants real-shaped agents). alt-cli-keep-inline forces tests to either duplicate the construction logic or import from cli/run.ts (which pulls in CLI side-effects). E18 already established that scripted-agent injection is the testing strategy; the factory extraction is a precondition." + } + }, + { + "local_id": 257, + "plane": "intent", + "kind": "decision", + "title": "Wire nudging as negative-constraint prompt injection, gated on nudgingActive.", + "body": "Wire nudging as negative-constraint prompt injection, gated on nudgingActive.", + "basis": "explicit", + "source": "[DEC6]", + "detail": { + "chosen_option": "When FrameRecord.nudgingActive is true, the clean-room agent prompt builder (clean-room.ts) injects a negative-constraint section listing the alternative selections from prior clean attempts in the same frame ('avoid these previously explored choices: …'). nudgingActive itself remains a frame-level flag set by the derivation loop after nudgeAfterN clean attempts; the new behavior is that fan-out reads the flag and the prior frame's reconciled outcomes when assembling the prompt for the next clean attempt.", + "rejected": [ + "Alternative: instead of injecting negative constraints, react to nudgingActive by raising sampling temperature on the LanguageModel call to encourage divergence.", + "Alternative: remove nudgingActive and nudgeAfterN entirely; rely on natural variance across N parallel clean-room runs to surface alternatives." + ], + "rationale": "T2 says 'retry feedback is schema-only', which forbids feeding the prior run's freeform output back; negative-constraint prompt injection is structurally compatible because it cites only stable schema-level alternatives that already exist in the graph. alt-nudging-temperature is opaque to the spec model — there's no way to express 'try harder' as a graph-level fact. alt-nudging-remove discards the existing FrameRecord field already plumbed through the loop (E14) and gives up the only existing mechanism for addressing repeat-output across attempts." + } + }, + { + "local_id": 258, + "plane": "intent", + "kind": "decision", + "title": "Adopt the six work-stream decomposition as the spec scope.", + "body": "Adopt the six work-stream decomposition as the spec scope.", + "basis": "explicit", + "source": "[DEC1]", + "detail": { + "chosen_option": "Decompose the code-health work into six work-streams driven by the grounding: (1) Derivation-loop correctness (P1, P2, P10/P32, P11, P34, C6); (2) Reference-integrity via schema-level NodeIdFromDisplayId (P30); (3) Engine-decoupled observability via Effect EventLog (RK5, X14); (4) Feature-model / SAT replacement of the impasse-centric divergence model (E32, E33, X16–X25, X32–X37, X44–X50); (5) Pure-logic test coverage and an end-to-end VCR integration test (P18–P25, X61); (6) Hygiene / refactor / doc fixes (P26, P27, P28, P29, P6).", + "rejected": [ + "Alternative: scope only to the open correctness items (P1, P2, P10/P32, P30) and explicitly defer the SAT/feature-model redesign and the EventLog migration.", + "Alternative: treat the work as a single big-bang rewrite — re-architect divergence handling, replace logging, redo references, and add tests in one merged change." + ], + "rationale": "X62 gives an explicit ordering (tests > correctness > design > rest) but also confirms all of these are in scope; the stakeholder design notes (X16–X60) explicitly call for the SAT/feature-model redesign and the EventLog migration as part of code-health, not as a separate spec. A correctness-only scope (alt-scope-correctness-only) would leave the dead code in the divergence model (P7, P8, P13, P17) un-addressed and contradict the stakeholder direction. A big-bang rewrite (alt-scope-bigbang) violates C1 (forward pass must keep working) and C4 (smoke-test artifacts must keep validating) because it would require simultaneous schema changes (X23) and behavioral changes. Six independent work-streams allow staged landing under C1/C4." + } + }, + { + "local_id": 259, + "plane": "intent", + "kind": "decision", + "title": "Build a custom DPLL with explanation instrumentation; reject off-the-shelf SAT and brute-force enumeration.", + "body": "Build a custom DPLL with explanation instrumentation; reject off-the-shelf SAT and brute-force enumeration.", + "basis": "explicit", + "source": "[DEC13]", + "detail": { + "chosen_option": "Implement the SAT solver as a small custom DPLL in TypeScript at src/engine/solver/dpll.ts (~200–400 LOC) with: unit propagation, pure-literal elimination, chronological backtracking, and an instrumentation hook that records, for each backtrack, which clauses caused the conflict. Public surface: validateModel(model), enumerateConfigurations(model, limit), backbone(model) returning per-axis {forcedValue, blockingClauses[]}, demotionCandidates(model) returning per-constraint {wouldSatisfy: boolean}. The solver imports nothing outside std and Effect.", + "rejected": [ + "Alternative: skip SAT entirely; for the stated scale of 5–20 axes × 2–5 alternatives, enumerate all assignments and filter by constraint formulas, computing backbone by intersection.", + "Alternative: depend on an existing SAT library (e.g., a JS port of MiniSat or logic-solver). Use its model enumeration and (where available) its proof/explanation API." + ], + "rationale": "X59 and X60 both require constraint-attribution explanations on backbone, which most off-the-shelf SAT libraries do not expose without a UNSAT-core extension we'd have to bolt on anyway (RK13). RK13 also flags Deno compatibility risk for off-the-shelf libraries. alt-brute-force-enumeration is correct for tiny inputs but X32 (demotion candidates: 'identifying which demotions would make it solvable') is most naturally expressed as 'try the formula minus this clause and re-solve' — trivial in DPLL, awkward as 'enumerate again with one fewer filter and re-intersect'. The custom DPLL also gives full control over the blockingClauses field that X59 explicitly asks for." + } + }, + { + "local_id": 260, + "plane": "intent", + "kind": "decision", + "title": "Compute baseline effects from real graph state, not from a hardcoded constant.", + "body": "Compute baseline effects from real graph state, not from a hardcoded constant.", + "basis": "explicit", + "source": "[DEC5]", + "detail": { + "chosen_option": "buildBaselineEffects in fan-in.ts reads the actual baseline node's commitment level (locked vs. provisional) from the graph and sets requiresAuthorization accordingly; locked baseline nodes produce {commitmentLevel: 'locked', requiresAuthorization: true}, provisional baseline nodes produce {commitmentLevel: 'provisional', requiresAuthorization: false}. The function takes the WorkingGraph plus the baselineFrameId / baseline node id set, not just the FanIn extraction result.", + "rejected": [ + "Alternative: leave baseline effects hardcoded to {locked, requiresAuthorization: true} and document this as a v1 simplification." + ], + "rationale": "X18 explicitly defines baseline effects as a distinct layer of the model: 'per-alternative authorization requirements'. With them hardcoded to locked/true, every fan-in run pretends the baseline is locked even when it is provisional, which makes the M_revision flow always demand revision authorization for changes to provisional content — directly contradicting X20's three-space semantics. C7 (no shortcuts) rules out alt-baseline-keep-hardcoded." + } + }, + { + "local_id": 261, + "plane": "intent", + "kind": "decision", + "title": "Honor the agent hint with strict upstream-only validation; otherwise default to one phase down.", + "body": "Honor the agent hint with strict upstream-only validation; otherwise default to one phase down.", + "basis": "explicit", + "source": "[DEC3]", + "detail": { + "chosen_option": "determineRewindPhase becomes a function determineRewindPhase(currentPhase, suggestedRewindPhase?) that prefers the agent's suggestedRewindPhase when present and strictly upstream of currentPhase, validating against the four-phase order (grounding < shaping < pinning < defining-done from X1) and falling back to 'one phase down' only when the hint is absent or invalid. It is plumbed through reconciliation.ts to where the recurse outcome is constructed.", + "rejected": [ + "Alternative: blindly trust the agent-supplied suggestedRewindPhase whenever set, with no acyclicity validation against currentPhase.", + "Alternative: keep determineRewindPhase as 'always one phase down' and instead remove suggestedRewindPhase from the agent schema as unused." + ], + "rationale": "X1 mandates strict derivational order; alt-rewind-trust-agent could let the agent push the loop forward or sideways, violating the support-edge acyclicity invariant in domain/invariants.ts. alt-rewind-always-one-down is what the code does today and is what P11/RK1 explicitly call broken — it discards information the agent already paid LLM tokens to compute (e.g., a missing-premise impasse that needs to rewind all the way to grounding, not just to the immediately previous phase). The validation guard makes the new behavior safe under C1 (forward pass keeps working when the hint is absent or stale)." + } + }, + { + "local_id": 262, + "plane": "intent", + "kind": "decision", + "title": "Delete FanInExtractionResult and introduce a new ConfigurationSpaceExtractionResult type.", + "body": "Delete FanInExtractionResult and introduce a new ConfigurationSpaceExtractionResult type.", + "basis": "explicit", + "source": "[DEC12]", + "detail": { + "chosen_option": "Define ConfigurationSpaceExtractionResult in src/domain/configuration.ts with first-class fields: axes: ReadonlyArray<{id, type: 'design'|'repair', cardinality: 'exactly_one'|'zero_or_one', label}>; alternatives: ReadonlyArray<{id, axisId, label}>; perRunStance: ReadonlyArray<{runId, axisId, alternativeId, stance: 'supports'|'contradicts'|'silent', rationale?}>; witnesses: ReadonlyArray<{runId, claimId, sourceSpan}>; candidateRepairs: ReadonlyArray<{contradictionId, alternativeIds, evidenceStrength}>; impasses: ReadonlyArray<{kind: 'authority_conflict'|'missing_premise'|..., conflictingNodes}>; hardConstraints: ReadonlyArray<{formula, witnessedBy: 'source_contradiction'|'dependency'|'grounded_rationale', citation}>. The previously-used FanInExtractionResult is deleted with no backward-compat shim.", + "rejected": [ + "Alternative: extend FanInExtractionResult with the new fields (axes, perRunStance, candidateRepairs) and keep the type name and existing import sites, providing a soft migration." + ], + "rationale": "X23 explicitly mandates 'delete with no backward compatibility'. The legacy type embeds the assumption that extraction produces hub-shaped records (P7: 'hasRepairSelections', 'hasRevisionRequirements' fields on SelectionOutcome), so an extension (alt-config-extend-fanin-schema) carries dead-load that would invite reuse of the old code path. C7 (no shortcuts) and the X16 redesign direction support the clean replacement; this is a prototype (G1, X4) so backward compatibility has no external consumers to protect." + } + }, + { + "local_id": 263, + "plane": "intent", + "kind": "decision", + "title": "Implement resume that re-enters the topmost open frame from the on-disk artifact; do not checkpoint mid-step.", + "body": "Implement resume that re-enters the topmost open frame from the on-disk artifact; do not checkpoint mid-step.", + "basis": "explicit", + "source": "[DEC22]", + "detail": { + "chosen_option": "For M5 resume/polish, do not implement mid-derivation checkpointing. The only checkpointable state is a completed checkpoint (T11: 'immutable snapshot when a full revision completes'); resume from anywhere else restarts the in-flight frame from its entry phase using the artifact-on-disk graph as the resume substrate. The CLI gains a `resume ` command that loads the latest WorkingGraph artifact, identifies the topmost open frame and earliest open impasse, and re-enters the derivation loop with that frame as parent.", + "rejected": [ + "Alternative: implement mid-derivation checkpoints — serialize WorkingGraph + frame stack + current chat after every reconciliation step, allowing exact resume mid-step.", + "Alternative: drop M5 resume entirely; require restarting from scratch on any failure." + ], + "rationale": "RK7 records the stakeholder preference for no mutable-state checkpoint dumps. T2 requires clean-room re-derivation to start with a fresh Chat anyway, so 'resuming a chat in progress' is meaningless inside a frame. The on-disk graph is already the source of truth (X3: pure JSON file I/O, no DB); re-entering at frame boundaries is the largest unit at which resume is well-defined. alt-resume-checkpoint-dumps directly contradicts RK7. alt-resume-skip is heavier than necessary given a 20-min-to-1-hr session length (X4) where a process death without resume costs significant LLM-spend." + } + }, + { + "local_id": 264, + "plane": "intent", + "kind": "decision", + "title": "Repair selection triggers JTMS-driven re-derivation in a new child frame; design selection remains monotone.", + "body": "Repair selection triggers JTMS-driven re-derivation in a new child frame; design selection remains monotone.", + "basis": "explicit", + "source": "[DEC17]", + "detail": { + "chosen_option": "After the user makes a repair selection on a Perspective record, the engine: (1) marks the un-chosen-side grounding nodes for the resolved contradiction as suspect, (2) calls markSuspectAndPropagate from each, (3) runs revisionImpact to compute the OUT set, (4) creates a new child frame whose entryPhase is the earliest-affected phase of any OUT node, (5) re-runs the derivation loop in that frame; reconciliation then merges the re-derived content with the existing graph by archiving OUT nodes (with supersededBy edges per X40) when their replacements are accepted.", + "rejected": [ + "Alternative: treat repair selection identically to design selection — monotone, no re-derivation — simplifying perspective-selection.ts at the cost of leaving downstream content derived from the un-chosen contradiction side intact." + ], + "rationale": "X20 explicitly distinguishes: 'design selection is monotone (no re-derivation needed), repair selection is non-monotone with respect to premises'. X45 reinforces the categorical split. alt-repair-monotone-likedesign produces a graph where downstream phases reference grounding facts the user has just rejected — exactly the silent-contradiction problem that motivated the entire feature-model redesign (E33). The new-child-frame approach (X39) keeps the re-derivation auditable and reuses the existing reconciliation machinery rather than a new in-place rewrite path." + } + }, + { + "local_id": 265, + "plane": "intent", + "kind": "decision", + "title": "Adopt the two-stage split with a hard schema boundary forbidding solver outputs from Stage 1.", + "body": "Adopt the two-stage split with a hard schema boundary forbidding solver outputs from Stage 1.", + "basis": "explicit", + "source": "[DEC11]", + "detail": { + "chosen_option": "Split fan-in into two distinct stages with separate file boundaries: Stage 1 LLM extraction (agents/fan-in.ts) emits a ConfigurationSpaceExtractionResult containing only canonical candidates, axes (with type + cardinality + label), alternatives, per-run stance (supports/contradicts/silent per run × axis × alternative), witness relations, candidate repairs, source-cited contradictions, and explicitly-witnessed hard constraints. Stage 2 deterministic solver analysis (engine/solver.ts + new engine/config-model.ts) consumes that result and computes: model validation, M_current/M_preview/M_revision spaces, backbone (mustSelect/mustDeselect with constraint-rule attribution), configuration enumeration, perspective generation. The Stage 1 schema explicitly disallows fields that would let the LLM pre-compute backbone, enumerate configurations, or scope impasses.", + "rejected": [ + "Alternative: extend the LLM extraction stage to also produce backbone and configuration enumeration so there is only one stage, eliminating the solver module.", + "Alternative: keep the existing single-stage FanInExtractionResult and patch it incrementally — add a structured stance field for P13, fix three-valued aggregation in-place, leave backbone and configuration enumeration where they are." + ], + "rationale": "X19 + X49 jointly mandate this split. The current single-stage design (alt-fanin-keep-single-stage) is what made E32's infinite-loop pathology possible: when the LLM 'decides' a contradiction it is computing backbone non-deterministically across runs, which is exactly the behavior X49 forbids. alt-fanin-llm-does-everything is incompatible with X49 and with C2 (deterministic tests) — backbone would no longer be a pure function of the extraction. The hard schema boundary (Stage 1 cannot return backbone fields) is what enforces the determinism property at compile time." + } + }, + { + "local_id": 266, + "plane": "intent", + "kind": "decision", + "title": "Adopt NodeIdFromDisplayId as a schema type with checkEffect-based decode against the live graph.", + "body": "Adopt NodeIdFromDisplayId as a schema type with checkEffect-based decode against the live graph.", + "basis": "explicit", + "source": "[DEC7]", + "detail": { + "chosen_option": "Introduce a NodeIdFromDisplayId schema type built on Schema.transformOrFail / SchemaGetter.checkEffect that decodes a display ID string against the live WorkingGraph and either yields the resolved NodeId or fails the schema decode with a structured error. Every agent IR field that today carries a displayId references this schema instead of plain string. Schema decode failures bubble up as Effect AI tool result errors so the LLM sees them on the next turn and retries; assembler.ts's silent post-hoc resolve-and-filter step is removed.", + "rejected": [ + "Alternative: keep displayId as plain string in the schema, but turn the silent .filter(undefined) in assembler.ts into a hard error that aborts the derivation step; surface it back to the agent via the existing reconciler retry path.", + "Alternative: deprecate displayId references in agent output entirely; require agents to emit only semanticKey references for upstream support, and have the assembler resolve semantic keys (which exist deterministically per emit batch)." + ], + "rationale": "X12 directly mandates this approach. The schema-level placement means failures appear in the natural Effect AI retry loop (LLM sees a structured tool error and retries) rather than requiring a custom retry path on top of assembler errors (alt-nodeid-validation-after-assembly), which is a parallel mechanism that duplicates Effect AI's own behavior. alt-nodeid-semantic-key-only is too restrictive: agents legitimately need to reference pre-existing graph nodes by their stable display ID across phases (e.g., a shaping design that supports a grounding constraint by ID), and semantic keys are scoped to a single derivation batch (X10). The schema-level approach addresses RK2 and P30 in the place where the contract between LLM and engine actually lives." + } + }, + { + "local_id": 267, + "plane": "intent", + "kind": "decision", + "title": "Default repair auto-resolution off in v1; surface every contradiction as a repair axis.", + "body": "Default repair auto-resolution off in v1; surface every contradiction as a repair axis.", + "basis": "explicit", + "source": "[DEC18]", + "detail": { + "chosen_option": "For v1 of the new fan-in, disable solver-side auto-resolution of repair precedence (X43): every detected contradiction becomes a repair axis presented to the user, regardless of evidence asymmetry. Auto-resolution becomes a follow-up pass once the repair flow is exercised in tests. The auto-resolution code path is feature-flagged (config.repairAutoResolve = false by default) rather than removed, so X43's design intent is preserved.", + "rejected": [ + "Alternative: ship X43's auto-resolution heuristic on by default in v1." + ], + "rationale": "RK18 explicitly suggests this for v1, citing correctness over user load. C7 (maximally correct, no shortcuts) prefers conservative behavior when the heuristic 'one repair option is clearly better-evidenced' has not yet been validated against real fan-in data. alt-repair-autoresolve-on risks silent decisions during the period when the repair flow is also new, compounding error sources during integration testing." + } + }, + { + "local_id": 268, + "plane": "intent", + "kind": "decision", + "title": "Persist blocking impasses as first-class graph nodes participating in JTMS.", + "body": "Persist blocking impasses as first-class graph nodes participating in JTMS.", + "basis": "explicit", + "source": "[DEC15]", + "detail": { + "chosen_option": "When the solver returns M_current as empty or unsatisfiable, the engine creates a first-class BlockingImpasse Impasse node (kind: 'unsatisfiable_configuration_space') in the graph with: support edges to all hard-constraint nodes that participate in the UNSAT core, status: open. The engine then surfaces demotionCandidates to the user; the user's chosen demotion is recorded as a relaxed_to edge from the BlockingImpasse node to the demoted constraint node. The blocking impasse remains in the graph after resolution (status: resolved) so it participates in the JTMS chain as a recorded decision point.", + "rejected": [ + "Alternative: model the blocking impasse as a transient diagnostic (an entry on the FanInRecord, not a graph node); resolve it inline and never persist it." + ], + "rationale": "X33 + X35 explicitly mandate this. RK19's open question is resolved by X33 in this grounding; the answer is 'persistent graph node'. alt-blocking-impasse-transient breaks X34 (the user's demotion choice must be auditable as an edge from the impasse) and X35 (must participate in provenance/JTMS), neither of which work for a transient record. Persisting it also gives the user a stable referent if the same constraint conflict recurs across revisions." + } + }, + { + "local_id": 269, + "plane": "intent", + "kind": "decision", + "title": "Create a fresh refined Impasse node and emit a refined_to lineage edge from the original to the refined one; mark the original superseded a…", + "body": "Create a fresh refined Impasse node and emit a refined_to lineage edge from the original to the refined one; mark the original superseded and recurse with the refined node id in spawnedImpasseIds.", + "basis": "explicit", + "source": "[DEC2]", + "detail": { + "chosen_option": "When the reconciler returns disposition: \"refined\" with a refinedImpasse payload, the reconciliation engine creates a new Impasse hub node in the graph (status: open), creates a refined_to lineage edge from the original impasse to the new one, marks the original as superseded, AND pushes the new node id onto spawnedImpasseIds so the recurse branch fires. This unifies P2's missing creation step with P1's missing population step.", + "rejected": [ + "Alternative: keep the original impasse as the live node and merely attach a refinedDescription field/edge to it instead of creating a new Impasse node, so refined_to is a self-annotation rather than a hub-to-hub edge.", + "Alternative: treat 'refined' as a terminal disposition that just marks the original impasse superseded without creating a successor; rely on the next pass through the loop to discover the residual problem fresh." + ], + "rationale": "X9 defines progress as 'incoming refined_to edges on unresolved impasses' — that progress signal only exists if refinement materializes a fresh node with an incoming refined_to edge, which alt-refined-as-edge-only does not produce. T9 distinguishes superseded as an impasse-status independent of lifecycle, which only makes sense if there is a successor to point at. alt-refined-as-resolved discards reconciler reasoning and forces re-discovery from scratch, contradicting C7 ('maximally correct, no shortcuts'). The chosen design also unifies cleanly with dec-recurse-wiring (single push site)." + } + }, + { + "local_id": 270, + "plane": "intent", + "kind": "decision", + "title": "Use a closed discriminated-union event catalog.", + "body": "Use a closed discriminated-union event catalog.", + "basis": "explicit", + "source": "[DEC9]", + "detail": { + "chosen_option": "Define a closed event catalog as a discriminated union under src/engine/events.ts: PhaseEntered, PhaseCompleted, FanOutAttempt(runIndex), FanInStarted, FanInExtractionCompleted, ConfigSpaceComputed(modelStats), PerspectiveGenerated, ReconcileOutcome(_tag: accepted|retry|recurse|refined), ImpasseSpawned(impasseId, kind), ImpasseResolved(impasseId, disposition), NudgeActivated(frameId, attemptCount), CowReplace(oldNodeId, newNodeId), SuspectPropagated(rootId, count), BlockingImpasseRaised(scope), UserInterventionRequested(kind), UserInterventionResolved(kind, choice). Every Console.log call in the engine maps to exactly one of these. CLI rendering, the JSON event log artifact, and the future web inspector consume the same union.", + "rejected": [ + "Alternative: keep events open-ended (string tag + free-form payload) so adding a new event doesn't require touching the union." + ], + "rationale": "X15 says granularity should be 'notable occurrences' — a closed union enforces that bar at the type system level (you have to add a tag, which makes you ask 'is this notable?'). Open-ended events (alt-eventlog-freeform) regress to the current Console.log situation where every author chooses ad hoc strings, defeating the point of typed events for downstream consumers (M7 web inspector, E7)." + } + }, + { + "local_id": 271, + "plane": "intent", + "kind": "decision", + "title": "Land doc fixes alongside the corresponding code changes.", + "body": "Land doc fixes alongside the corresponding code changes.", + "basis": "explicit", + "source": "[DEC21]", + "detail": { + "chosen_option": "Update PLAN.md in three places: (1) artifact layout section now lists graph/reconciliation-records.json (P28); (2) resolved design question #10 is updated to nudge_after_n default = 1, matching X42 and the implementation (P29, E36); (3) the post-redesign sections describing fan-in, perspectives, and impasses are rewritten to reflect the feature-model / SAT model (X16) and the deletion of FanInExtractionResult (X23).", + "rejected": [ + "Alternative: defer doc fixes to after the implementation lands; PLAN.md is internal and the discrepancies are minor." + ], + "rationale": "PLAN.md is referenced by the spec workflow (E6) and discrepancies between PLAN and code are exactly the class of error PROBLEMS.md tracks (P28, P29). Lettings docs drift (alt-doc-fix-defer) regenerates the same defect class. The change is minor enough to land per-PR with the corresponding code change." + } + }, + { + "local_id": 272, + "plane": "intent", + "kind": "decision", + "title": "Populate justifications and wire revisionImpact into the reconciliation engine.", + "body": "Populate justifications and wire revisionImpact into the reconciliation engine.", + "basis": "explicit", + "source": "[DEC16]", + "detail": { + "chosen_option": "Populate the justifications field on derived nodes during assembly: assembler.ts, when creating a derived node, builds a justifications array with one entry per Justification/Decision/Impasse hub the node is connected to, recording {hubId, premiseIds: [...]}. solver.ts's revisionImpact function is called from the reconciliation engine whenever an upstream grounding node's review status flips to suspect, returning the closure of OUT (tainted) nodes per X28. The OUT closure is fed into the re-derivation flow (dec-cow-wiring).", + "rejected": [ + "Alternative: delete revisionImpact and the empty justifications field as dead code (P8/P17), formalize re-derivation as 'always re-run the affected phase clean' without belief revision." + ], + "rationale": "X28 + X29 + X38 specify JTMS-style propagation as the chosen mechanism for taint after repair selection. alt-jtms-remove-dead-code is feasible but conflicts with C7 (no shortcuts) and X40 (graph grows monotonically; superseded nodes retained with supersededBy edges) — retaining superseded nodes only makes sense if downstream consumers can distinguish IN from OUT, which is precisely what JTMS provides. The justifications + revisionImpact pair is already implemented and tested (E13, E17); the missing piece is connecting it." + } + }, + { + "local_id": 273, + "plane": "intent", + "kind": "decision", + "title": "Demote Perspective to a record; remove it from the hub-node kind union.", + "body": "Demote Perspective to a record; remove it from the hub-node kind union.", + "basis": "explicit", + "source": "[DEC10]", + "detail": { + "chosen_option": "Demote Perspective from a hub-node kind in the graph to a plain record (struct) attached to the FanInRecord (or sibling DerivationRunRecord) artifact written under graph/. Edges that today point to a Perspective hub are removed; perspective summaries are referenced by id within the record store and rendered by the CLI/inspector. The graph schema's hub-kind union (T8: Justification, Decision, Impasse) is unchanged; perspective ceases to be one.", + "rejected": [ + "Alternative: keep Perspective as a hub node but mark it 'epistemically inert' so reconciliation never derives support through it." + ], + "rationale": "X11 is the explicit stakeholder preference. Hub nodes carry epistemic weight (T8: 'make joint causation explicit') — a hub that is by definition inert (alt-perspective-keep-hub) is a category error and a footgun for the reconciler, which would have to special-case 'this hub kind doesn't propagate support'. Records sit naturally next to FanInRecord and DerivationRunRecord (already in graph/), and the CLI already renders records via formatHandoffReport-shaped code (E27)." + } + }, + { + "local_id": 274, + "plane": "intent", + "kind": "decision", + "title": "Wire cowReplace + markSuspectAndPropagate into the grounding-enrichment + intervention paths.", + "body": "Wire cowReplace + markSuspectAndPropagate into the grounding-enrichment + intervention paths.", + "basis": "explicit", + "source": "[DEC4]", + "detail": { + "chosen_option": "Immediately after every cowReplace call, the engine calls markSuspectAndPropagate(oldNodeId) which traverses identity-preserving lineage edges (equivalent_to, merged_into per X53) and sets review status to suspect on all transitively reachable nodes. The derivation loop reads suspect status to decide which downstream phases need re-derivation in the next iteration.", + "rejected": [ + "Alternative: delete cowReplace and markSuspectAndPropagate as YAGNI dead code, since they have no callers (E31).", + "Alternative: keep grounding-enrichment as additive-only — always append new grounding nodes, never replace, and rely on the reconciler to archive obsolete originals; do not call cowReplace." + ], + "rationale": "C6 makes this a blocking prerequisite for derivation-loop signoff (\"the derivation loop cannot be signed off without it\"). X57 makes the same claim normatively. T10 specifies COW semantics for grounding, so alt-cow-mutate-in-place contradicts the stated substrate model and would force the reconciler to do double work to discover obsolete originals. alt-cow-delete-orphan-methods directly violates C6 and the X16–X40 stakeholder direction. Combined wiring of both functions is required because cowReplace alone leaves downstream nodes still pointing at superseded premises with clean status — markSuspectAndPropagate is what keeps the JTMS chain (X28) consistent." + } + }, + { + "local_id": 275, + "plane": "intent", + "kind": "decision", + "title": "Stage A–E in dependency order with parallelism between independent stages.", + "body": "Stage A–E in dependency order with parallelism between independent stages.", + "basis": "explicit", + "source": "[DEC23]", + "detail": { + "chosen_option": "Land the work in five staged increments, each independently mergeable while keeping the forward pass green (C1) and the smoke artifacts validating (C4): Stage A (correctness wiring) — dec-recurse-wiring + dec-refined-impasse + dec-rewind-phase + dec-baseline-effects + dec-nudging + dec-cow-wiring + dec-jtms (population only); Stage B (reference integrity) — dec-nodeid-from-displayid; Stage C (observability) — dec-eventlog + dec-eventlog-catalog; Stage D (feature-model redesign) — dec-fanin-two-stage + dec-config-schema-replace + dec-solver-impl + dec-perspective-record + dec-perspective-generation + dec-blocking-impasse + dec-repair-flow + dec-repair-autoresolve; Stage E (test + hygiene) — dec-test-strategy + dec-cli-extract + dec-doc-fixes + dec-resume. Stages A–C and E can land in any order; D depends on A's JTMS wiring and B's NodeIdFromDisplayId.", + "rejected": [ + "Alternative: follow X62's stated priority order strictly — land all of P18–P25 (tests) first, then correctness, then design, then everything else — with no parallel staging." + ], + "rationale": "Strict X62 priority ordering (alt-staging-priority-strict) puts tests first, but dec-test-strategy depends on dec-cli-extract (factory injection for module tests, design-cli-extract-modules), which is itself a hygiene item ranked lower in X62. The dependency graph forces some interleaving. The proposed staging respects the spirit of X62 (correctness items P1/P2/P10/P32 land in Stage A early; tests land in Stage E across all the modules now made testable) while allowing independent landings. C1/C4 are preserved per stage because each stage's changes are scoped: A patches existing wiring, B is a schema change with retry behavior, C is an observability swap, D is the redesign behind a feature flag during transition, E is additive tests + refactors." + } + }, + { + "local_id": 276, + "plane": "intent", + "kind": "decision", + "title": "Migrate the engine to Effect EventLog with typed events; the CLI becomes one subscriber.", + "body": "Migrate the engine to Effect EventLog with typed events; the CLI becomes one subscriber.", + "basis": "explicit", + "source": "[DEC8]", + "detail": { + "chosen_option": "Replace every Console.log call in src/engine/** (fan-in.ts, fan-out.ts, phase-runner.ts, reconciliation.ts, derivation-loop.ts, perspective-selection.ts, assembler.ts) with an Effect EventLog (effect/EventLog) emission that publishes a typed, tagged event ({_tag: 'FanInStarted'|'FanInCompleted'|'FanOutAttempt'|'ReconcileOutcome'|'PhaseEntered'|'ImpasseSpawned'|'ImpasseResolved'|'CowReplace'|'SuspectPropagated'|'NudgeActivated'|...}, payload). The CLI (cli/run.ts and cli-driver.ts) becomes a subscriber that renders these events to stdout via its existing formatHandoffReport-style code paths. The engine no longer imports Console.", + "rejected": [ + "Alternative: keep Console.log but funnel it through a single helper module so the coupling is at one place; defer EventLog migration to post-prototype.", + "Alternative: replace Console.log with Effect.logInfo / Effect.logDebug structured logging (the X13 fallback option) without introducing an EventLog topic and subscriber model." + ], + "rationale": "X14 is an explicit stakeholder preference for EventLog specifically over Effect.logInfo. X15 calls for events on notable occurrences — i.e. domain-meaningful events like 'ImpasseSpawned', not log levels — which is the EventLog model and not the Effect.logInfo model (alt-eventlog-effect-logger), where consumers cannot dispatch on _tag. alt-eventlog-keep-console preserves the current coupling to a CLI presentation layer (RK5) and is incompatible with the planned web inspector (M7, E7) which needs a structured stream of engine events to render. The X13/X14/X15 chain is monotone in stakeholder preference toward EventLog; X14 is the latest preference and supersedes the X13 fallback." + } + }, + { + "local_id": 277, + "plane": "intent", + "kind": "decision", + "title": "Use farthest-first / k-medoids over Hamming distance on axis vectors with k=3 per space.", + "body": "Use farthest-first / k-medoids over Hamming distance on axis vectors with k=3 per space.", + "basis": "explicit", + "source": "[DEC14]", + "detail": { + "chosen_option": "Generate perspective summaries by sampling configurations from the SAT solver's enumeration (capped at e.g. 200 per space) and running farthest-first / k-medoids over Hamming distance on axis-assignment vectors to pick k=3 representatives per space (M_current and M_preview separately). Each representative becomes a Perspective record carrying: the configuration vector, a default-bundle status flag (display only), and a short LLM-generated label. evaluateSelection runs against any user-chosen bundle and is the only readiness gate.", + "rejected": [ + "Alternative: use a clustering algorithm (e.g., k-means with one-hot embedding) instead of k-medoids; centroids are not real configurations, so generate the closest real configuration to each centroid as the representative.", + "Alternative: surface every distinct configuration in M_current up to a bound; let the UI handle scrolling." + ], + "rationale": "X21 directly specifies this method. k-medoids returns real configurations, which fits dec-perspective-record (the record has to point at a real activatable configuration); alt-perspective-clustering needs a 'snap to nearest real config' post-step that is just k-medoids in disguise. alt-perspective-show-all defeats X33's notion of 'perspective' as a digestible summary and would overwhelm the user when M_preview is large. k=3 is a conservative default given RK17's openness; making k a parameter is left for tuning." + } + } +] diff --git a/.fixtures/seed-specs/bilal-port/code-health/spec.json b/.fixtures/seed-specs/bilal-port/code-health/spec.json new file mode 100644 index 00000000..d175d6c1 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/code-health/spec.json @@ -0,0 +1,5 @@ +{ + "slug": "code-health", + "name": "Code Health", + "readiness_grade": "commitments_ready" +} diff --git a/.fixtures/seed-specs/bilal-port/explorer-ui/edges.json b/.fixtures/seed-specs/bilal-port/explorer-ui/edges.json new file mode 100644 index 00000000..e25da6d0 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/explorer-ui/edges.json @@ -0,0 +1,4914 @@ +[ + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 143, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 193, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 204, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 244, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 276, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 8, + "target_local_id": 264, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 243, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 186, + "target_local_id": 4, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 252, + "target_local_id": 219, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 142, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 55, + "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": 76, + "target_local_id": 150, + "stance": null, + "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": 248, + "target_local_id": 138, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 278, + "target_local_id": 166, + "stance": null, + "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": 123, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 272, + "target_local_id": 161, + "stance": null, + "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": 114, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 156, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 22, + "target_local_id": 111, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 54, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 46, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 118, + "target_local_id": 34, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 3, + "target_local_id": 267, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 139, + "target_local_id": 165, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 38, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 183, + "target_local_id": 241, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 25, + "target_local_id": 233, + "stance": null, + "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": 248, + "target_local_id": 240, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 266, + "target_local_id": 280, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 168, + "target_local_id": 271, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 270, + "target_local_id": 70, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 189, + "target_local_id": 249, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 119, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 243, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 212, + "target_local_id": 42, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 145, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 131, + "target_local_id": 277, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 84, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 180, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 231, + "target_local_id": 162, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 256, + "target_local_id": 201, + "stance": null, + "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": 32, + "target_local_id": 271, + "stance": null, + "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": 175, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 175, + "target_local_id": 162, + "stance": null, + "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": 132, + "target_local_id": 264, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 184, + "target_local_id": 6, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 116, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 62, + "target_local_id": 122, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 48, + "target_local_id": 101, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 269, + "stance": null, + "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": 267, + "target_local_id": 260, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 262, + "target_local_id": 280, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 244, + "target_local_id": 135, + "stance": null, + "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, + "target_local_id": 81, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 23, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 256, + "target_local_id": 263, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 39, + "target_local_id": 273, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 118, + "target_local_id": 42, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 272, + "target_local_id": 79, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 67, + "target_local_id": 247, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 42, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 102, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 160, + "target_local_id": 273, + "stance": null, + "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": 32, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 113, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 279, + "target_local_id": 245, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 185, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 194, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 206, + "stance": null, + "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": 177, + "target_local_id": 230, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 13, + "target_local_id": 103, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 56, + "target_local_id": 277, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 236, + "target_local_id": 230, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 126, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 133, + "target_local_id": 165, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 197, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 94, + "target_local_id": 108, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 200, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 230, + "target_local_id": 240, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 10, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 258, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 68, + "target_local_id": 13, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 280, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 162, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 236, + "target_local_id": 240, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 131, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 45, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 19, + "target_local_id": 216, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 254, + "stance": null, + "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": 110, + "target_local_id": 254, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 155, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 272, + "target_local_id": 170, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 118, + "target_local_id": 103, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 208, + "target_local_id": 241, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 250, + "target_local_id": 5, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 255, + "target_local_id": 279, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 198, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 162, + "target_local_id": 268, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 21, + "target_local_id": 263, + "stance": null, + "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": 114, + "target_local_id": 271, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 136, + "target_local_id": 219, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 171, + "target_local_id": 250, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 5, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 279, + "target_local_id": 114, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 234, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 154, + "target_local_id": 41, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 66, + "target_local_id": 127, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 148, + "target_local_id": 273, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 141, + "target_local_id": 78, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 132, + "target_local_id": 84, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 62, + "target_local_id": 87, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 13, + "stance": null, + "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": 10, + "target_local_id": 107, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 194, + "target_local_id": 165, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 126, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 105, + "target_local_id": 7, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 156, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 207, + "target_local_id": 264, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 249, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 56, + "target_local_id": 59, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 278, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 85, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 63, + "stance": null, + "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, + "target_local_id": 268, + "stance": null, + "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, + "target_local_id": 202, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 95, + "target_local_id": 197, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 16, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 37, + "target_local_id": 71, + "stance": null, + "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": 97, + "target_local_id": 48, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 9, + "target_local_id": 37, + "stance": null, + "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": 43, + "target_local_id": 256, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 92, + "target_local_id": 205, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 226, + "target_local_id": 163, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 6, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 61, + "target_local_id": 235, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 219, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 88, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 98, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 32, + "target_local_id": 245, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 39, + "target_local_id": 128, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 206, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 122, + "target_local_id": 103, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 276, + "target_local_id": 76, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 119, + "target_local_id": 162, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 77, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 274, + "target_local_id": 278, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 155, + "target_local_id": 54, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 257, + "target_local_id": 152, + "stance": null, + "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": 230, + "target_local_id": 85, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 256, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 63, + "target_local_id": 244, + "stance": "for", + "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": 164, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 233, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 225, + "target_local_id": 180, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 267, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 115, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 276, + "target_local_id": 235, + "stance": null, + "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": 181, + "target_local_id": 271, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 168, + "target_local_id": 255, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 205, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 152, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 73, + "target_local_id": 194, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 121, + "target_local_id": 235, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 171, + "target_local_id": 5, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 246, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 88, + "target_local_id": 120, + "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": 246, + "target_local_id": 85, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 61, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 87, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 173, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 196, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 50, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 11, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 200, + "target_local_id": 205, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 240, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 211, + "target_local_id": 238, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 48, + "target_local_id": 263, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 280, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 264, + "target_local_id": 64, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 175, + "target_local_id": 116, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 135, + "target_local_id": 117, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 79, + "target_local_id": 27, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 34, + "target_local_id": 215, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 122, + "target_local_id": 87, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 276, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 118, + "target_local_id": 251, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 170, + "target_local_id": 79, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 38, + "target_local_id": 135, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 193, + "target_local_id": 280, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 13, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 119, + "target_local_id": 116, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 179, + "target_local_id": 81, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 141, + "target_local_id": 49, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 9, + "target_local_id": 71, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 150, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 197, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 168, + "target_local_id": 245, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 49, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 240, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 8, + "target_local_id": 64, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 162, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 80, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 127, + "target_local_id": 228, + "stance": null, + "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, + "target_local_id": 254, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 201, + "target_local_id": 216, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 270, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 37, + "target_local_id": 7, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 267, + "target_local_id": 280, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 22, + "target_local_id": 259, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 30, + "target_local_id": 23, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 252, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 170, + "stance": null, + "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": 262, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 138, + "target_local_id": 190, + "stance": null, + "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": 203, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 192, + "target_local_id": 14, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 202, + "target_local_id": 64, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 169, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 136, + "target_local_id": 52, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 73, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 267, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 215, + "target_local_id": 228, + "stance": null, + "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, + "target_local_id": 273, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 225, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 109, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 20, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 265, + "target_local_id": 242, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 273, + "target_local_id": 193, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 76, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 190, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 33, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 57, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 277, + "target_local_id": 59, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 20, + "target_local_id": 243, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 182, + "target_local_id": 150, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 213, + "target_local_id": 114, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 20, + "target_local_id": 194, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 25, + "target_local_id": 267, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 165, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 267, + "target_local_id": 14, + "stance": null, + "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, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 210, + "target_local_id": 170, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 111, + "target_local_id": 235, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 169, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 66, + "target_local_id": 98, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 255, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 96, + "target_local_id": 87, + "stance": null, + "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": 96, + "target_local_id": 82, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 237, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 110, + "target_local_id": 28, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 188, + "target_local_id": 53, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 18, + "target_local_id": 273, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 188, + "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": 251, + "target_local_id": 103, + "stance": null, + "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": 122, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 174, + "target_local_id": 275, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 48, + "target_local_id": 216, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 159, + "target_local_id": 52, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 265, + "target_local_id": 52, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 99, + "target_local_id": 135, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 256, + "target_local_id": 237, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 126, + "target_local_id": 280, + "stance": "for", + "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, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 251, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 250, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 197, + "target_local_id": 170, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 18, + "target_local_id": 106, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 189, + "target_local_id": 28, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 21, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 38, + "target_local_id": 206, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 29, + "target_local_id": 113, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 241, + "target_local_id": 150, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 27, + "target_local_id": 117, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 231, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 166, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 110, + "target_local_id": 193, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 184, + "target_local_id": 212, + "stance": null, + "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, + "target_local_id": 54, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 78, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 82, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 10, + "target_local_id": 191, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 5, + "target_local_id": 260, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 93, + "target_local_id": 52, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 140, + "target_local_id": 279, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 2, + "target_local_id": 86, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 163, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 127, + "target_local_id": 98, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 229, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 220, + "target_local_id": 271, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 203, + "target_local_id": 70, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 86, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 33, + "target_local_id": 49, + "stance": null, + "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, + "target_local_id": 265, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 234, + "target_local_id": 101, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 274, + "target_local_id": 280, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 110, + "target_local_id": 204, + "stance": "for", + "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": 274, + "target_local_id": 229, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 97, + "target_local_id": 234, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 63, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 16, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 7, + "target_local_id": 244, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 35, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 260, + "target_local_id": 117, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 44, + "target_local_id": 38, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 205, + "target_local_id": 229, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 67, + "target_local_id": 143, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 144, + "target_local_id": 206, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 118, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 8, + "target_local_id": 202, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 271, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 142, + "target_local_id": 264, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 38, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 30, + "target_local_id": 14, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 116, + "target_local_id": 143, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 87, + "target_local_id": 34, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 147, + "target_local_id": 202, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 159, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 7, + "target_local_id": 135, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 185, + "target_local_id": 53, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 80, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 15, + "target_local_id": 181, + "stance": null, + "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, + "target_local_id": 139, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 149, + "target_local_id": 279, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 270, + "target_local_id": 31, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 105, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 249, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 245, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 183, + "target_local_id": 250, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 178, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 269, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 50, + "target_local_id": 114, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 65, + "target_local_id": 254, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 21, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 151, + "target_local_id": 126, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 212, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 254, + "target_local_id": 14, + "stance": null, + "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": 125, + "target_local_id": 31, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 247, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 125, + "target_local_id": 143, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 55, + "target_local_id": 204, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 87, + "target_local_id": 194, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 146, + "target_local_id": 263, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 261, + "stance": null, + "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, + "target_local_id": 237, + "stance": null, + "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, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 144, + "target_local_id": 38, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 26, + "target_local_id": 263, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 132, + "target_local_id": 263, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 146, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 71, + "target_local_id": 244, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 270, + "target_local_id": 143, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 178, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 198, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 273, + "target_local_id": 133, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 3, + "target_local_id": 233, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 55, + "target_local_id": 193, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 220, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 138, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 170, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 158, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 160, + "target_local_id": 133, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 138, + "target_local_id": 246, + "stance": null, + "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, + "target_local_id": 156, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 76, + "target_local_id": 53, + "stance": null, + "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, + "target_local_id": 191, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 234, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 113, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 104, + "target_local_id": 37, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 165, + "target_local_id": 117, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 190, + "target_local_id": 204, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 206, + "target_local_id": 244, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 38, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 23, + "target_local_id": 14, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 65, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 24, + "target_local_id": 165, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 123, + "target_local_id": 47, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 213, + "target_local_id": 271, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 206, + "target_local_id": 63, + "stance": null, + "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, + "target_local_id": 182, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 151, + "target_local_id": 254, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 187, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 139, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 90, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 257, + "target_local_id": 275, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 130, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 106, + "target_local_id": 24, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 232, + "target_local_id": 224, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 60, + "target_local_id": 233, + "stance": null, + "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, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 182, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 57, + "target_local_id": 185, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 163, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 35, + "target_local_id": 111, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 269, + "target_local_id": 277, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 214, + "target_local_id": 142, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 212, + "target_local_id": 34, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 157, + "target_local_id": 51, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 202, + "target_local_id": 84, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 26, + "target_local_id": 146, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 258, + "target_local_id": 59, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 127, + "target_local_id": 34, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 254, + "target_local_id": 126, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 193, + "target_local_id": 247, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 128, + "target_local_id": 273, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 197, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 251, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 57, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 206, + "target_local_id": 135, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 6, + "target_local_id": 212, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 41, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 91, + "target_local_id": 245, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 107, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 140, + "target_local_id": 70, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 271, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 238, + "target_local_id": 7, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 164, + "target_local_id": 70, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 93, + "target_local_id": 159, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 241, + "target_local_id": 268, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 160, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 72, + "target_local_id": 266, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 226, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 95, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 204, + "target_local_id": 280, + "stance": "for", + "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, + "target_local_id": 84, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 102, + "target_local_id": 138, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 112, + "target_local_id": 276, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 216, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 93, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 181, + "target_local_id": 261, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 15, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 28, + "target_local_id": 247, + "stance": null, + "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": 263, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 254, + "target_local_id": 201, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 133, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 158, + "target_local_id": 198, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 90, + "target_local_id": 80, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 73, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 155, + "target_local_id": 22, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 148, + "target_local_id": 106, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 59, + "target_local_id": 131, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 254, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 48, + "target_local_id": 234, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 22, + "stance": null, + "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, + "target_local_id": 270, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 45, + "target_local_id": 237, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 144, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 76, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 266, + "target_local_id": 246, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 13, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 29, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 245, + "target_local_id": 255, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 196, + "target_local_id": 278, + "stance": null, + "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, + "target_local_id": 185, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 174, + "target_local_id": 152, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 78, + "target_local_id": 49, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 201, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 204, + "target_local_id": 247, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 56, + "target_local_id": 269, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 34, + "target_local_id": 74, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 266, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 130, + "target_local_id": 41, + "stance": null, + "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, + "target_local_id": 98, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 127, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 192, + "target_local_id": 70, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 117, + "target_local_id": 199, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 19, + "target_local_id": 48, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 268, + "target_local_id": 224, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 271, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 178, + "target_local_id": 210, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 105, + "target_local_id": 238, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 81, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 91, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 187, + "target_local_id": 76, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 273, + "target_local_id": 100, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 2, + "target_local_id": 182, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 220, + "target_local_id": 162, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 40, + "target_local_id": 278, + "stance": "for", + "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, + "target_local_id": 216, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 104, + "target_local_id": 71, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 192, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 270, + "target_local_id": 125, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 210, + "target_local_id": 161, + "stance": null, + "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, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 238, + "target_local_id": 143, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 12, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 109, + "target_local_id": 49, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 219, + "target_local_id": 52, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 172, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 268, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 166, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 58, + "target_local_id": 112, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 275, + "target_local_id": 14, + "stance": null, + "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, + "target_local_id": 274, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 273, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 230, + "target_local_id": 262, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 248, + "target_local_id": 246, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 262, + "target_local_id": 76, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 190, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 39, + "target_local_id": 160, + "stance": null, + "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, + "target_local_id": 145, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 13, + "target_local_id": 251, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 274, + "target_local_id": 100, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 267, + "target_local_id": 53, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 190, + "target_local_id": 280, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 4, + "target_local_id": 253, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 180, + "target_local_id": 55, + "stance": null, + "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, + "target_local_id": 194, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 87, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 89, + "target_local_id": 27, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 208, + "target_local_id": 268, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 263, + "target_local_id": 52, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 272, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 115, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 150, + "target_local_id": 224, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 4, + "target_local_id": 262, + "stance": null, + "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, + "target_local_id": 193, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 68, + "target_local_id": 118, + "stance": null, + "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, + "target_local_id": 172, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 50, + "target_local_id": 279, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 240, + "target_local_id": 150, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 86, + "target_local_id": 182, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 270, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 167, + "target_local_id": 31, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 164, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 6, + "target_local_id": 253, + "stance": null, + "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, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 132, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 181, + "target_local_id": 232, + "stance": null, + "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, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 126, + "target_local_id": 14, + "stance": null, + "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, + "target_local_id": 234, + "stance": null, + "basis": "explicit", + "rationale": null + } +] diff --git a/.fixtures/seed-specs/bilal-port/explorer-ui/nodes.json b/.fixtures/seed-specs/bilal-port/explorer-ui/nodes.json new file mode 100644 index 00000000..a6b97484 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/explorer-ui/nodes.json @@ -0,0 +1,2937 @@ +[ + { + "local_id": 1, + "plane": "oracle", + "kind": "check", + "title": "Explorer UI — code-audit pass", + "body": "Synthetic parent check representing the manual code-audit pass during which evidence nodes were authored. Generated by .fixtures/seed-specs/bilal-port/_port-script.ts to give imported evidence a structural parent on the oracle plane.", + "basis": "explicit", + "source": "derived-port-synthetic", + "detail": null + }, + { + "local_id": 2, + "plane": "intent", + "kind": "criterion", + "title": "A CSS scanline overlay must be present in the DOM as a pseudo-element or overlay div positioned above the WebGL canvas at all times.", + "body": "A CSS scanline overlay must be present in the DOM as a pseudo-element or overlay div positioned above the WebGL canvas at all times. Its computed style must include pointer-events: none so that mouse and touch events pass through to the canvas beneath. The overlay must be visible as a subtle horizontal stripe pattern when inspected visually against a bright node.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR23]", + "detail": null + }, + { + "local_id": 3, + "plane": "intent", + "kind": "criterion", + "title": "The ForceAtlas2 layout computation must execute in a Web Worker, not on the main UI thread.", + "body": "The ForceAtlas2 layout computation must execute in a Web Worker, not on the main UI thread. Verified by: opening browser DevTools performance timeline during artifact load, confirming that the layout computation task appears on a Worker thread and not on the Main thread. The main thread must remain responsive (no tasks exceeding 50ms) during layout computation.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR26]", + "detail": null + }, + { + "local_id": 4, + "plane": "intent", + "kind": "requirement", + "title": "In the micro-view graph, node shape must encode node kind: content nodes must render as circles, hub nodes must render as diamonds.", + "body": "In the micro-view graph, node shape must encode node kind: content nodes must render as circles, hub nodes must render as diamonds.", + "basis": "accepted_review_set", + "source": "technical-inferred [R12]", + "detail": null + }, + { + "local_id": 5, + "plane": "intent", + "kind": "context", + "title": "The stakeholder intends nodes to be colored by derivation phase in the macro graph visualization.", + "body": "The stakeholder intends nodes to be colored by derivation phase in the macro graph visualization.", + "basis": "explicit", + "source": "stakeholder [X31]", + "detail": null + }, + { + "local_id": 6, + "plane": "intent", + "kind": "requirement", + "title": "The Connections section of the detail panel for a justification hub node must render: (1) a PREMISES group showing all nodes connected by '…", + "body": "The Connections section of the detail panel for a justification hub node must render: (1) a PREMISES group showing all nodes connected by 'informed_by' edges; (2) a CONCLUSIONS group showing all nodes connected by 'produced' edges. Each node reference must be a clickable pill that navigates the detail panel to that node.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R36]", + "detail": null + }, + { + "local_id": 7, + "plane": "intent", + "kind": "context", + "title": "The artifact bundler script (scripts/bundle-artifact.ts) includes a placeholder for the FrameRecord summary field: when bundling, it reads…", + "body": "The artifact bundler script (scripts/bundle-artifact.ts) includes a placeholder for the FrameRecord summary field: when bundling, it reads frames.json and adds summary: null for each frame if no summary is present. The UI's FrameRecord type declares summary as string | null. When the elicitation pipeline is extended to produce summaries (resolving RK5/E5), the bundler will populate this field and the UI will render it without code changes. This design makes the dependency on the pipeline schema extension explicit and non-blocking.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D25]", + "detail": null + }, + { + "local_id": 8, + "plane": "intent", + "kind": "criterion", + "title": "The Sigma WebGL canvas element must have no keydown, keyup, or keypress event listeners attached directly to it or via React synthetic even…", + "body": "The Sigma WebGL canvas element must have no keydown, keyup, or keypress event listeners attached directly to it or via React synthetic events. Verified by inspecting event listeners on the canvas DOM element using getEventListeners() in DevTools or a test spy, confirming zero keyboard event handlers are registered.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR74]", + "detail": null + }, + { + "local_id": 9, + "plane": "intent", + "kind": "criterion", + "title": "The src/types/artifact.ts file must contain zero import statements referencing the spec-elicitation package or any Deno-specific module.", + "body": "The src/types/artifact.ts file must contain zero import statements referencing the spec-elicitation package or any Deno-specific module. Running tsc --noEmit on the spec-elicitation-ui package must complete with zero errors, confirming the type definitions are self-contained. Verified by static analysis of the import graph rooted at src/types/artifact.ts.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR89]", + "detail": null + }, + { + "local_id": 10, + "plane": "intent", + "kind": "context", + "title": "The spec-elicitation-ui package lives at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation-ui/ as a sibling…", + "body": "The spec-elicitation-ui package lives at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation-ui/ as a sibling to spec-elicitation (X13). It is a standard Vite + React + TypeScript project with the following top-level structure: src/components/ (React UI components), src/store/ (Zustand store and derived index builders), src/graph/ (Sigma.js setup, custom WebGL programs, ForceAtlas2 worker), src/macro/ (WebGL macro timeline renderer), src/types/ (TypeScript types mirroring the artifact.json schema), src/utils/ (artifact parser, diff utilities, provenance traversal), and scripts/bundle-artifact.ts (the Deno bundler script that produces artifact.json, kept here rather than in spec-elicitation to colocate it with the schema it produces). Tailwind config extends the base with the phosphor CRT theme tokens defined in crt-design-system-design.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D21]", + "detail": null + }, + { + "local_id": 11, + "plane": "intent", + "kind": "context", + "title": "The current spec.md output renders all 376+ nodes sequentially and is unusable for understanding relationships, tracing provenance, or navi…", + "body": "The current spec.md output renders all 376+ nodes sequentially and is unusable for understanding relationships, tracing provenance, or navigating decisions.", + "basis": "explicit", + "source": "stakeholder [X8]", + "detail": null + }, + { + "local_id": 12, + "plane": "intent", + "kind": "context", + "title": "Displaying interventions in both the node detail panel and the macro timeline is the most complete approach but carries higher implementati…", + "body": "Displaying interventions in both the node detail panel and the macro timeline is the most complete approach but carries higher implementation cost.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK4]", + "detail": null + }, + { + "local_id": 13, + "plane": "intent", + "kind": "requirement", + "title": "The Connections section of the detail panel for a decision hub node must render: (1) a RATIONALE block showing the decision's rationale pro…", + "body": "The Connections section of the detail panel for a decision hub node must render: (1) a RATIONALE block showing the decision's rationale prose with a phosphor-amber left border; (2) a CONSIDERED group listing all nodes reached via 'considered' edges as clickable displayId pills; (3) a SELECTED group with a green glow indicator showing chosen alternatives via 'selected' edges; (4) a REJECTED group with a dimmed red indicator showing rejected alternatives via 'rejected' edges; (5) a CONSEQUENCES group listing nodes reached via 'consequence' or 'produced' edges. Each pill must navigate the detail panel to the referenced node on click. A 'Trace to grounding' button must highlight the support-edge subgraph back to grounding-phase nodes in the main Sigma canvas.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R34]", + "detail": null + }, + { + "local_id": 14, + "plane": "intent", + "kind": "constraint", + "title": "Real-time updates are out of scope.", + "body": "Real-time updates are out of scope. The artifact is loaded once at startup and does not change during the session.", + "basis": "explicit", + "source": "stakeholder [C4]", + "detail": null + }, + { + "local_id": 15, + "plane": "intent", + "kind": "criterion", + "title": "After successful artifact parse, the application must play a CRT power-on animation before displaying any graph content.", + "body": "After successful artifact parse, the application must play a CRT power-on animation before displaying any graph content. The animation must implement the keyframe sequence opacity 0 → 0.4 → 0.1 → 1 over approximately 150ms as a CSS @keyframes animation, and must not use a slide-in transition.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR5]", + "detail": null + }, + { + "local_id": 16, + "plane": "intent", + "kind": "term", + "title": "The macro view is the temporal history view showing derivation frames and their…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T16]", + "detail": { + "definition": "The macro view is the temporal history view showing derivation frames and their relationships over time, laid out as a vertical timeline branching horizontally at derivation loops." + } + }, + { + "local_id": 17, + "plane": "intent", + "kind": "criterion", + "title": "When a fan-in grouping contains more than one node pair, the comparison overlay must show a tab row above the split columns allowing the us…", + "body": "When a fan-in grouping contains more than one node pair, the comparison overlay must show a tab row above the split columns allowing the user to navigate between all node pairs in the grouping. Verified using the reference artifact's fan-in-records.json: selecting a grouping with multiple nodeIds (e.g. 'cloud-agnostic-context' with nodeIds 7cf067d6 and 9d1a93f3) must produce a tab for each pair.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR66]", + "detail": null + }, + { + "local_id": 18, + "plane": "intent", + "kind": "criterion", + "title": "The baseline/candidate comparison view must be triggerable from exactly two entry points: (1) clicking a fan-in record entry in the macro v…", + "body": "The baseline/candidate comparison view must be triggerable from exactly two entry points: (1) clicking a fan-in record entry in the macro view, which must open the comparison for that fan-in grouping; (2) clicking a 'Compare' button in the detail panel of any node with lifecycle='candidate', which must open the comparison for the fan-in grouping containing that candidate node. Both entry points must produce the same comparison overlay UI.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR64]", + "detail": null + }, + { + "local_id": 19, + "plane": "intent", + "kind": "criterion", + "title": "When a node has reviewStatus._tag='suspect', the Identity section must display a 'suspect' indicator with clickable links to each causeId.", + "body": "When a node has reviewStatus._tag='suspect', the Identity section must display a 'suspect' indicator with clickable links to each causeId. When reviewStatus._tag='conditional', the Identity section must display a 'conditional' indicator with clickable links to each impasseId. A 'clean' node must show a clean indicator with no extra links. Verified by mounting the detail panel for nodes with each review status variant.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR43]", + "detail": null + }, + { + "local_id": 20, + "plane": "intent", + "kind": "requirement", + "title": "The macro timeline must visually encode the full regression/recovery narrative: (1) the initial frame trunk rendered in phosphor-green; (2)…", + "body": "The macro timeline must visually encode the full regression/recovery narrative: (1) the initial frame trunk rendered in phosphor-green; (2) rederive frames in phosphor-amber; (3) impasse nodes referenced by triggerImpasseIds shown as warning-colored hexagonal badges on branch edges; (4) perspective hub nodes shown as small purple indicator badges on their associated frame cards; (5) the nudgingActive flag shown as a 'NUDGED' badge. Together these elements must make the impasse → rederive → fan-out → reconciliation cycle legible without additional explanation.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R45]", + "detail": null + }, + { + "local_id": 21, + "plane": "intent", + "kind": "criterion", + "title": "Clicking any node in the micro-view graph must activate the right detail panel with a CSS @keyframes flicker animation.", + "body": "Clicking any node in the micro-view graph must activate the right detail panel with a CSS @keyframes flicker animation. The animation must pulse opacity through the sequence 0 → 0.4 → 0.1 → 1 and complete within approximately 150ms (±20ms). The panel must not slide in from the side. Verified by: recording a click event in a test renderer and asserting the applied animation name matches the flicker keyframes definition with the correct duration.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR40]", + "detail": null + }, + { + "local_id": 22, + "plane": "intent", + "kind": "requirement", + "title": "The micro-view toolbar must contain a snapshot slider that scrubs through SnapshotRecord revisions.", + "body": "The micro-view toolbar must contain a snapshot slider that scrubs through SnapshotRecord revisions. The slider must display a numeric revision badge and a timestamp label for the current snapshot. A status line below the slider must show the current revision number and the associated frameId(s).", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R21]", + "detail": null + }, + { + "local_id": 23, + "plane": "intent", + "kind": "requirement", + "title": "The application must provide no mechanism to create, edit, or delete nodes or edges.", + "body": "The application must provide no mechanism to create, edit, or delete nodes or edges. All data displayed must come exclusively from the loaded artifact.json and must not change during the session.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R5]", + "detail": null + }, + { + "local_id": 24, + "plane": "intent", + "kind": "term", + "title": "Baseline refers to the active, reconciled state of the knowledge graph; candida…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T19]", + "detail": { + "definition": "Baseline refers to the active, reconciled state of the knowledge graph; candidate refers to nodes produced during clean-room re-derivation branches before reconciliation. Side-by-side comparison of baseline vs candidate nodes is a required UI feature." + } + }, + { + "local_id": 25, + "plane": "intent", + "kind": "criterion", + "title": "After the initial layout computation completes for a given specId and snapshotRevision, the layout positions must be written to sessionStor…", + "body": "After the initial layout computation completes for a given specId and snapshotRevision, the layout positions must be written to sessionStorage under a key incorporating the specId and snapshotRevision. On a second load of the same artifact within the same browser session, no Web Worker layout computation must occur — the cached positions must be read directly from sessionStorage and applied to the Sigma graph.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR28]", + "detail": null + }, + { + "local_id": 26, + "plane": "intent", + "kind": "criterion", + "title": "The right detail panel must render exactly four collapsible sections in top-to-bottom order: (1) Identity, (2) Connections, (3) Provenance,…", + "body": "The right detail panel must render exactly four collapsible sections in top-to-bottom order: (1) Identity, (2) Connections, (3) Provenance, (4) Validation. The Identity section must be expanded by default and must remain visible even when other sections are collapsed. Sections 2, 3, and 4 must toggle open/closed independently. Verified by mounting the panel for a known node and asserting four section headers are present, with Identity expanded and the others collapsible.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR41]", + "detail": null + }, + { + "local_id": 27, + "plane": "intent", + "kind": "context", + "title": "The pipeline artifact output is a directory containing JSON files organized into: graph/ (nodes, edges, frames, derivation-runs, fan-in rec…", + "body": "The pipeline artifact output is a directory containing JSON files organized into: graph/ (nodes, edges, frames, derivation-runs, fan-in records, snapshots), and top-level files (manifest, sources, extracted-claims, interventions), plus rendered views (spec.md, prose.md) and reports (validation, handoff summary).", + "basis": "explicit", + "source": "external-observed [X5]", + "detail": null + }, + { + "local_id": 28, + "plane": "oracle", + "kind": "evidence", + "title": "The smoke-webhook reference artifact contains 4 derivation phases with 3 derivation loop attempts, and includes decision, justification, im…", + "body": "The smoke-webhook reference artifact contains 4 derivation phases with 3 derivation loop attempts, and includes decision, justification, impasse, and perspective hub nodes.", + "basis": "explicit", + "source": "external-observed [E3]", + "detail": null + }, + { + "local_id": 29, + "plane": "intent", + "kind": "criterion", + "title": "Clicking a frame card in the macro timeline must open a modal listing which nodes changed in that frame (the node-diff list).", + "body": "Clicking a frame card in the macro timeline must open a modal listing which nodes changed in that frame (the node-diff list). The modal must display at minimum the displayId, lifecycle, and phase of each node associated with that frame. The modal must be dismissible via Escape or a close control. No WebGL zoom-into-frame transition may be attempted; the modal is the required behavior for the current iteration.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR62]", + "detail": null + }, + { + "local_id": 30, + "plane": "intent", + "kind": "criterion", + "title": "Inspection of the rendered DOM must reveal zero input controls, buttons, or form elements that create, modify, or delete any node or edge.", + "body": "Inspection of the rendered DOM must reveal zero input controls, buttons, or form elements that create, modify, or delete any node or edge. No mutation of the in-memory graph store may occur after the initial artifact load; all node and edge data must remain identical to the parsed artifact.json for the duration of the session.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR7]", + "detail": null + }, + { + "local_id": 31, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers that all pipeline output files be combined into a single artifact.json file that is loaded into the visualization p…", + "body": "The stakeholder prefers that all pipeline output files be combined into a single artifact.json file that is loaded into the visualization program.", + "basis": "explicit", + "source": "stakeholder [X17]", + "detail": null + }, + { + "local_id": 32, + "plane": "intent", + "kind": "criterion", + "title": "Given a valid artifact.json file dropped onto the landing page drop zone, the application must parse the file entirely in the browser using…", + "body": "Given a valid artifact.json file dropped onto the landing page drop zone, the application must parse the file entirely in the browser using the File API (no network request made), transition away from the landing page, and display the main explorer view — all without any server upload or URL entry by the user.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR1]", + "detail": null + }, + { + "local_id": 33, + "plane": "intent", + "kind": "criterion", + "title": "When any filter or search is active, matching nodes must render at full Sigma glow intensity and non-matching nodes must render at approxim…", + "body": "When any filter or search is active, matching nodes must render at full Sigma glow intensity and non-matching nodes must render at approximately 15% opacity in the canvas. Edges where both endpoints are non-matching must also be visually dimmed. No node or edge may be removed from the graphology graph during filtering — the total node and edge count in the graphology instance must remain constant before and after any filter is applied.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR36]", + "detail": null + }, + { + "local_id": 34, + "plane": "intent", + "kind": "context", + "title": "The 17 edge types across 6 categories are: hub-generic (informed_by, produced); decision (considered, selected, rejected, consequence); imp…", + "body": "The 17 edge types across 6 categories are: hub-generic (informed_by, produced); decision (considered, selected, rejected, consequence); impasse (conflicting_input, resolved_by, spawned, refined_to); perspective (aggregates); content (derived_from, depends_on, conflicts_with, motivates, defines, references, satisfied_by, operationalized_by, alternative_to, revises_baseline); lineage (equivalent_to, refined_by, weakened_by, strengthened_by, split_into, merged_into, obsoleted_by). Content edge category has 10 types; lineage has 7.", + "basis": "explicit", + "source": "technical-observed [X44]", + "detail": null + }, + { + "local_id": 35, + "plane": "intent", + "kind": "term", + "title": "The micro view is the lineage-focused graph view showing the spec at a particul…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T15]", + "detail": { + "definition": "The micro view is the lineage-focused graph view showing the spec at a particular point in time, with inactive nodes grayed out and a snapshot selector for time-scrubbing." + } + }, + { + "local_id": 36, + "plane": "intent", + "kind": "criterion", + "title": "For each intervention record in the reference artifact's interventions.json, every nodeId in its targetNodeIds array must appear as a key i…", + "body": "For each intervention record in the reference artifact's interventions.json, every nodeId in its targetNodeIds array must appear as a key in the interventionsByNodeId index mapping to an array that includes that intervention. Specifically: node 7cbf0826 must map to intervention 0f60db54; node 38c2ff0b must map to intervention 158ac3c4; node 61d9201c must map to intervention 926c3761; node cb3857aa must map to intervention 610c95d1. Each association must be verified by direct index lookup.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR87]", + "detail": null + }, + { + "local_id": 37, + "plane": "intent", + "kind": "requirement", + "title": "The application must define TypeScript types for all artifact.json structures in src/types/artifact.ts.", + "body": "The application must define TypeScript types for all artifact.json structures in src/types/artifact.ts. NodeRecord must be a discriminated union on kind ('content' | 'hub'). FrameRecord must include summary as string | null to future-proof the optional per-frame LLM summary field. These types must be defined independently of the spec-elicitation package (no cross-package import of Effect schemas).", + "basis": "accepted_review_set", + "source": "technical-inferred [R24]", + "detail": null + }, + { + "local_id": 38, + "plane": "intent", + "kind": "requirement", + "title": "Each frame card in the macro timeline must display: frame mode badge (initial / rederive), entryPhase label, attemptNumber, nudgingActive i…", + "body": "Each frame card in the macro timeline must display: frame mode badge (initial / rederive), entryPhase label, attemptNumber, nudgingActive indicator (shown as a 'NUDGED' badge when true), createdAt timestamp, and the pre-generated LLM summary text when present. When no summary is present (summary is null), the summary region must display a muted 'NO SUMMARY AVAILABLE' placeholder in dimmed monospace style. No runtime error or broken layout may result from an absent summary.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R43]", + "detail": null + }, + { + "local_id": 39, + "plane": "intent", + "kind": "criterion", + "title": "The comparison overlay must render as a split panel with a left column showing the baseline node and a right column showing the candidate n…", + "body": "The comparison overlay must render as a split panel with a left column showing the baseline node and a right column showing the candidate node. Differences in text, semanticRole, epistemicStatus, and authority fields must be highlighted using a line-diff style with phosphor-colored additions (green) and deletions (red/amber). The fan-in grouping rationale must appear as a prominent decision banner between the two columns.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR65]", + "detail": null + }, + { + "local_id": 40, + "plane": "intent", + "kind": "context", + "title": "A search must highlight matching nodes in the graph AND simultaneously show a results list in a side panel.", + "body": "A search must highlight matching nodes in the graph AND simultaneously show a results list in a side panel.", + "basis": "explicit", + "source": "stakeholder [X38]", + "detail": null + }, + { + "local_id": 41, + "plane": "intent", + "kind": "requirement", + "title": "The main explorer must render a three-region resizable split layout: a left sidebar containing the filter/search panel and results list, a…", + "body": "The main explorer must render a three-region resizable split layout: a left sidebar containing the filter/search panel and results list, a central canvas area hosting either the micro or macro view, and a right detail panel. All three regions must be simultaneously visible when a node is selected. Panels must be resizable via drag handles.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R7]", + "detail": null + }, + { + "local_id": 42, + "plane": "intent", + "kind": "goal", + "title": "The explorer UI must replace the current unnavigable flat spec.md output, enabling users to understand relationships, trace provenance, and…", + "body": "The explorer UI must replace the current unnavigable flat spec.md output, enabling users to understand relationships, trace provenance, and navigate decisions.", + "basis": "explicit", + "source": "stakeholder [G2]", + "detail": null + }, + { + "local_id": 43, + "plane": "intent", + "kind": "criterion", + "title": "The Validation section of the detail panel must not appear in the DOM when the selected node has reviewStatus._tag='clean' and no validatio…", + "body": "The Validation section of the detail panel must not appear in the DOM when the selected node has reviewStatus._tag='clean' and no validation errors touch its incident edges. When errors are present, the section must list each error showing: rule, severity, message, edge type, and edge direction. Verified by: selecting a clean node and asserting the Validation section is absent; then selecting a node with incident errored edges and asserting the section is present with correct error details.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR53]", + "detail": null + }, + { + "local_id": 44, + "plane": "intent", + "kind": "criterion", + "title": "Each frame card rendered in the macro timeline must display all of the following fields: mode badge ('initial' or 'rederive'), entryPhase l…", + "body": "Each frame card rendered in the macro timeline must display all of the following fields: mode badge ('initial' or 'rederive'), entryPhase label, attemptNumber, createdAt timestamp, and a nudgingActive indicator rendered as a 'NUDGED' badge when nudgingActive=true. For the reference artifact, the two rederive frames with attemptNumber=1 and attemptNumber=2 (ids b40fd568 and b9236ccf) must show 'NUDGED'. The frame with attemptNumber=0 (id 10f07753) must not show 'NUDGED'.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR57]", + "detail": null + }, + { + "local_id": 45, + "plane": "intent", + "kind": "criterion", + "title": "The edgeIssuesByNodeId index must correctly associate validation errors with both the source and target node of each errored edge.", + "body": "The edgeIssuesByNodeId index must correctly associate validation errors with both the source and target node of each errored edge. Using the first validation error in the reference artifact (edgeId 00452e1e, a derived_from edge between nodes b66575fc and 6c45100b), both node IDs must be present as keys in edgeIssuesByNodeId, each mapping to an array containing that error. Querying an unrelated node ID must return an empty array.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR80]", + "detail": null + }, + { + "local_id": 46, + "plane": "intent", + "kind": "requirement", + "title": "The right detail panel must activate on node click with a CRT power-on flicker animation of approximately 150ms duration, implemented as a…", + "body": "The right detail panel must activate on node click with a CRT power-on flicker animation of approximately 150ms duration, implemented as a CSS @keyframes sequence that pulses opacity 0 → 0.4 → 0.1 → 1. The panel must not use a slide-in transition.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R30]", + "detail": null + }, + { + "local_id": 47, + "plane": "intent", + "kind": "requirement", + "title": "In the macro timeline, edges between frames must be visually encoded by relationship type: trunk-to-branch edges triggered by an impasse mu…", + "body": "In the macro timeline, edges between frames must be visually encoded by relationship type: trunk-to-branch edges triggered by an impasse must be drawn in warning amber and labeled with the triggerImpasseId as a displayId badge; fan-in record edges connecting rederive frames back to the baseline must be drawn in bright green and labeled 'RECONCILED'.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R44]", + "detail": null + }, + { + "local_id": 48, + "plane": "intent", + "kind": "requirement", + "title": "The Identity section of the detail panel must display: for content nodes — semanticRole, epistemicStatus, and authority; for hub nodes — hu…", + "body": "The Identity section of the detail panel must display: for content nodes — semanticRole, epistemicStatus, and authority; for hub nodes — hubType. It must also show the review status as a tagged indicator: 'clean' with no annotation, 'suspect' with links to causeIds, and 'conditional' with links to impasseIds.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R32]", + "detail": null + }, + { + "local_id": 49, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers that when a search or filter is active, matching nodes glow at full intensity while non-matching nodes are rendered…", + "body": "The stakeholder prefers that when a search or filter is active, matching nodes glow at full intensity while non-matching nodes are rendered at low opacity (~15%), with edges also dimmed when both endpoints are non-matching, multiple filters using AND logic, and the graph topology preserved so context is not lost.", + "basis": "explicit", + "source": "stakeholder [X27]", + "detail": null + }, + { + "local_id": 50, + "plane": "intent", + "kind": "criterion", + "title": "When the application is deployed to a remote static host (e.g., GitHub Pages) and accessed with ?artifact= pointing to a CORS-enabled…", + "body": "When the application is deployed to a remote static host (e.g., GitHub Pages) and accessed with ?artifact= pointing to a CORS-enabled artifact.json URL, the application must load and parse the artifact via fetch() and enter the main explorer view without requiring any local file selection. No error related to file system access may occur. Verified by deploying the built app to a static host and testing the URL param flow end-to-end.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR84]", + "detail": null + }, + { + "local_id": 51, + "plane": "intent", + "kind": "requirement", + "title": "All interactive HTML elements (buttons, filter chips, panel headers, results list rows) must have hover states that intensify glow via CSS…", + "body": "All interactive HTML elements (buttons, filter chips, panel headers, results list rows) must have hover states that intensify glow via CSS transition on box-shadow and text-shadow. No interactive element may have a visually inert hover state.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R55]", + "detail": null + }, + { + "local_id": 52, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers that the provenance section in the node detail panel renders a small Sigma.js subgraph showing the full upstream de…", + "body": "The stakeholder prefers that the provenance section in the node detail panel renders a small Sigma.js subgraph showing the full upstream derivation chain, with clickable nodes for navigation, visually coherent with the main graph.", + "basis": "explicit", + "source": "stakeholder [X25]", + "detail": null + }, + { + "local_id": 53, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers a WebGL-based renderer (e.g.", + "body": "The stakeholder prefers a WebGL-based renderer (e.g. Sigma.js) for graph rendering because it can handle tens of thousands of nodes and edges interactively and enables more ambitious visual design such as a phosphor-glow effect on nodes.", + "basis": "explicit", + "source": "stakeholder [X16]", + "detail": null + }, + { + "local_id": 54, + "plane": "intent", + "kind": "requirement", + "title": "When the snapshot selector changes the active snapshot, node visibility must be updated by adjusting node opacity rather than removing node…", + "body": "When the snapshot selector changes the active snapshot, node visibility must be updated by adjusting node opacity rather than removing nodes from the graph. Nodes not in the selected snapshot's activeNodeIds array must be rendered at near-zero opacity. Layout positions must not be recomputed on snapshot change.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R20]", + "detail": null + }, + { + "local_id": 55, + "plane": "intent", + "kind": "context", + "title": "The full reference graph (376+ active nodes, 2,662 edges, plus archived and candidate nodes) may be too large to render interactively witho…", + "body": "The full reference graph (376+ active nodes, 2,662 edges, plus archived and candidate nodes) may be too large to render interactively without deliberate performance optimization.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK1]", + "detail": null + }, + { + "local_id": 56, + "plane": "intent", + "kind": "criterion", + "title": "When Macro view is active, a dedicated element separate from the Sigma micro-view canvas must be mounted in the central area with…", + "body": "When Macro view is active, a dedicated element separate from the Sigma micro-view canvas must be mounted in the central area with a WebGL rendering context (getContext('webgl') or getContext('webgl2') returning non-null). The Sigma canvas must not be present in the DOM simultaneously. Verified by querying the DOM for canvas elements while in each view mode and asserting exactly one WebGL canvas is present per mode.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR55]", + "detail": null + }, + { + "local_id": 57, + "plane": "intent", + "kind": "criterion", + "title": "With the smoke-webhook reference artifact loaded (761 total nodes, 2,662 edges), panning and zooming the micro-view Sigma canvas must susta…", + "body": "With the smoke-webhook reference artifact loaded (761 total nodes, 2,662 edges), panning and zooming the micro-view Sigma canvas must sustain a frame rate of at least 30 fps as measured by browser DevTools performance profiling. No interaction (pan, zoom, hover) may produce a jank frame exceeding 100ms on a mid-range developer machine.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR15]", + "detail": null + }, + { + "local_id": 58, + "plane": "intent", + "kind": "criterion", + "title": "When no node is selected, the right detail panel must have zero computed width (or be absent from the DOM) and the central canvas must expa…", + "body": "When no node is selected, the right detail panel must have zero computed width (or be absent from the DOM) and the central canvas must expand to fill the full remaining width after the left sidebar. Selecting a node must cause the detail panel to appear; deselecting must collapse it again.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR11]", + "detail": null + }, + { + "local_id": 59, + "plane": "intent", + "kind": "requirement", + "title": "The macro view must be rendered on a dedicated WebGL canvas separate from the Sigma micro-view canvas, implemented using raw WebGL with a t…", + "body": "The macro view must be rendered on a dedicated WebGL canvas separate from the Sigma micro-view canvas, implemented using raw WebGL with a thin abstraction layer (not a graph library). This canvas must be mounted in place of the Sigma canvas when macro view is active.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R41]", + "detail": null + }, + { + "local_id": 60, + "plane": "intent", + "kind": "criterion", + "title": "While ForceAtlas2 layout computation is in progress, the central canvas must display a CRT-styled text indicator reading 'COMPUTING LAYOUT.…", + "body": "While ForceAtlas2 layout computation is in progress, the central canvas must display a CRT-styled text indicator reading 'COMPUTING LAYOUT...' (or equivalent). The indicator must disappear and be replaced by the rendered graph when the Worker posts its result. No partially-rendered or unlaid-out graph may be shown while computation is ongoing.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR27]", + "detail": null + }, + { + "local_id": 61, + "plane": "intent", + "kind": "requirement", + "title": "The top toolbar must contain a view-mode toggle that switches the central canvas between Micro view and Macro view.", + "body": "The top toolbar must contain a view-mode toggle that switches the central canvas between Micro view and Macro view. The snapshot selector must be visible in the toolbar only when Micro view is active.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R8]", + "detail": null + }, + { + "local_id": 62, + "plane": "intent", + "kind": "criterion", + "title": "In the micro-view graph, every hub node with hubType='impasse' must render with a hexagonal shape, visually distinct from the diamond used…", + "body": "In the micro-view graph, every hub node with hubType='impasse' must render with a hexagonal shape, visually distinct from the diamond used for other hub types. Inspecting the rendered geometry of a known impasse node from the reference artifact (e.g., the node with id '557db0a8-5b5b-4ab9-97e2-4ac5c4f243d5') must confirm the hexagonal form.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR18]", + "detail": null + }, + { + "local_id": 63, + "plane": "intent", + "kind": "context", + "title": "FrameRecord does not currently include a summary field.", + "body": "FrameRecord does not currently include a summary field. The macro view's per-frame summary display depends on a schema extension to the elicitation pipeline that has not yet been implemented.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | stakeholder-inferred [RK5]", + "detail": null + }, + { + "local_id": 64, + "plane": "intent", + "kind": "constraint", + "title": "Keyboard navigation does not apply to the graph canvas.", + "body": "Keyboard navigation does not apply to the graph canvas. Canvas interaction is mouse/touch only.", + "basis": "explicit", + "source": "stakeholder [C11]", + "detail": null + }, + { + "local_id": 65, + "plane": "intent", + "kind": "criterion", + "title": "The interventionsByNodeId index must correctly map each nodeId appearing in any intervention's targetNodeIds array to that intervention rec…", + "body": "The interventionsByNodeId index must correctly map each nodeId appearing in any intervention's targetNodeIds array to that intervention record. Using the reference artifact's interventions.json (4 records, each with one targetNodeId), querying the index for each of the four targetNodeIds must return the corresponding intervention. Querying a nodeId that appears in no intervention must return an empty array, not undefined or an error.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR25]", + "detail": null + }, + { + "local_id": 66, + "plane": "intent", + "kind": "criterion", + "title": "In the micro-view graph, edges must be rendered in three visually distinct colors by category: support edges (derived_from, depends_on, inf…", + "body": "In the micro-view graph, edges must be rendered in three visually distinct colors by category: support edges (derived_from, depends_on, informed_by) in dim amber; workflow edges (produced, considered, selected, rejected, consequence, conflicting_input, resolved_by, spawned, refined_to, aggregates) in brighter green; structural edges (conflicts_with, motivates, defines, references, satisfied_by, operationalized_by, alternative_to, revises_baseline, and all lineage edge types) in muted cyan. Sampling 10 edges of each category from the reference artifact and reading their rendered colors must confirm the correct category mapping for all 30 sampled edges.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR19]", + "detail": null + }, + { + "local_id": 67, + "plane": "intent", + "kind": "context", + "title": "The smoke-webhook artifact directory contains: graph/ (nodes.json, edges.json, frames.json, derivation-runs.json, fan-in-records.json, snap…", + "body": "The smoke-webhook artifact directory contains: graph/ (nodes.json, edges.json, frames.json, derivation-runs.json, fan-in-records.json, snapshots.json), plus top-level manifest.json, sources.json, extracted-claims.json, interventions.json, reports/validation.json, reports/handoff-summary.md, and views/. Nodes carry id, displayId, specId, frameId, phase, text, lifecycle, reviewStatus, provenance, createdAt, kind, and kind-specific fields (semanticRole/epistemicStatus/authority for content; hubType/rationale for hubs). Edges carry id, source.nodeId, target.nodeId, type, rationale, provenance, createdAt. Frames carry parentFrameId, baselineFrameId, entryPhase, triggerImpasseIds, mode (initial/rederive), attemptNumber, nudgingActive. No summary field exists on FrameRecord.", + "basis": "explicit", + "source": "technical-observed [X43]", + "detail": null + }, + { + "local_id": 68, + "plane": "intent", + "kind": "criterion", + "title": "Clicking the 'Trace to grounding' button in a decision hub's Connections section must traverse support edges from the decision's considered…", + "body": "Clicking the 'Trace to grounding' button in a decision hub's Connections section must traverse support edges from the decision's considered nodes back to grounding-phase nodes and apply the active-filter highlighting model (full intensity for traversed nodes, ~15% opacity for all others) to the main Sigma canvas. The Zustand filterState must reflect this subgraph highlight. Clearing the filter must restore all nodes to normal opacity.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR46]", + "detail": null + }, + { + "local_id": 69, + "plane": "intent", + "kind": "constraint", + "title": "Running the elicitation pipeline from within the UI is out of scope.", + "body": "Running the elicitation pipeline from within the UI is out of scope.", + "basis": "explicit", + "source": "stakeholder [C3]", + "detail": null + }, + { + "local_id": 70, + "plane": "intent", + "kind": "constraint", + "title": "The app has no backend.", + "body": "The app has no backend. All data is loaded from static JSON files. The app must be deployable as a static site.", + "basis": "explicit", + "source": "stakeholder [C2]", + "detail": null + }, + { + "local_id": 71, + "plane": "intent", + "kind": "context", + "title": "The UI defines TypeScript types for all artifact.json structures in src/types/artifact.ts, mirroring the domain model from the spec-elicita…", + "body": "The UI defines TypeScript types for all artifact.json structures in src/types/artifact.ts, mirroring the domain model from the spec-elicitation package without importing it directly (to avoid a cross-package dependency on Deno-specific Effect schemas). Key types: ArtifactFile (top-level), GraphData, NodeRecord (discriminated union on kind: 'content' | 'hub'), ContentNode (with semanticRole, epistemicStatus, authority), HubNode (with hubType, rationale), EdgeRecord, FrameRecord (with optional summary?: string to future-proof RK5), SnapshotRecord, DerivationRunRecord, FanInRecord, InterventionRecord, ValidationReport, ValidationError. The discriminated union on NodeRecord.kind enables exhaustive type-narrowing in the detail panel renderer.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D22]", + "detail": null + }, + { + "local_id": 72, + "plane": "intent", + "kind": "criterion", + "title": "When the snapshot slider is moved to a revision where a given node is not in the activeNodeIds array, that node must remain present in the…", + "body": "When the snapshot slider is moved to a revision where a given node is not in the activeNodeIds array, that node must remain present in the Sigma graphology graph instance but be rendered at near-zero opacity (visually invisible). The node must not be removed from the graphology graph. Layout positions must not change when the slider is moved. Verified by: querying the graphology instance for a known inactive-at-revision node and asserting it exists with a near-zero opacity attribute.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR30]", + "detail": null + }, + { + "local_id": 73, + "plane": "intent", + "kind": "criterion", + "title": "The macro timeline must make the full impasse→rederive→fan-out→reconciliation narrative legible through five simultaneous visual cues: (1)…", + "body": "The macro timeline must make the full impasse→rederive→fan-out→reconciliation narrative legible through five simultaneous visual cues: (1) initial frame trunk in phosphor-green; (2) rederive frames in phosphor-amber; (3) impasse nodes referenced by triggerImpasseIds shown as warning-colored hexagonal badges on branch edges; (4) perspective hub nodes shown as small purple indicator badges on their associated frame cards; (5) nudgingActive shown as a 'NUDGED' badge. All five cues must be present simultaneously in the rendered macro view for the reference artifact.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR60]", + "detail": null + }, + { + "local_id": 74, + "plane": "intent", + "kind": "context", + "title": "Edge types are defined in a dedicated file at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation/src/domain/e…", + "body": "Edge types are defined in a dedicated file at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation/src/domain/edge-types.ts.", + "basis": "explicit", + "source": "stakeholder-observed [X12]", + "detail": null + }, + { + "local_id": 75, + "plane": "intent", + "kind": "criterion", + "title": "Nodes in the micro-view graph must exhibit a visible phosphor glow effect implemented via a WebGL fragment shader.", + "body": "Nodes in the micro-view graph must exhibit a visible phosphor glow effect implemented via a WebGL fragment shader. Hovering over a node must produce a measurably increased glow radius or intensity relative to the idle state. Selecting a node must produce a further-increased glow intensity relative to hover. The glow color must match the node's derivation-phase color.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR21]", + "detail": null + }, + { + "local_id": 76, + "plane": "intent", + "kind": "context", + "title": "The stakeholder envisions nodes emitting a color-appropriate phosphor glow implemented as a WebGL fragment shader, with hover states intens…", + "body": "The stakeholder envisions nodes emitting a color-appropriate phosphor glow implemented as a WebGL fragment shader, with hover states intensifying the glow effect.", + "basis": "explicit", + "source": "stakeholder [X40]", + "detail": null + }, + { + "local_id": 77, + "plane": "intent", + "kind": "criterion", + "title": "The snapshot selector control must appear in the toolbar if and only if Micro view is active.", + "body": "The snapshot selector control must appear in the toolbar if and only if Micro view is active. Switching to Macro view must remove the snapshot selector from the DOM (or hide it such that it receives no pointer events). Switching back to Micro must restore it.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR13]", + "detail": null + }, + { + "local_id": 78, + "plane": "intent", + "kind": "requirement", + "title": "When multiple filter controls are active simultaneously, the application must combine them using AND logic: a node is considered matching o…", + "body": "When multiple filter controls are active simultaneously, the application must combine them using AND logic: a node is considered matching only if it satisfies every active filter dimension.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R26]", + "detail": null + }, + { + "local_id": 79, + "plane": "intent", + "kind": "term", + "title": "An intervention is a record of a human action that occurred during a derivation…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T13]", + "detail": { + "definition": "An intervention is a record of a human action that occurred during a derivation frame, stored in interventions.json in the artifact." + } + }, + { + "local_id": 80, + "plane": "intent", + "kind": "requirement", + "title": "Nodes that have one or more validation errors touching their incident edges must be rendered in the micro-view graph with a red-tinted glow…", + "body": "Nodes that have one or more validation errors touching their incident edges must be rendered in the micro-view graph with a red-tinted glow halo in addition to their normal phase-color glow, implemented as a second glow pass in the WebGL shader.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R17]", + "detail": null + }, + { + "local_id": 81, + "plane": "intent", + "kind": "requirement", + "title": "All node text, displayIds, data values, and code-like content must use a monospaced font (JetBrains Mono or equivalent).", + "body": "All node text, displayIds, data values, and code-like content must use a monospaced font (JetBrains Mono or equivalent). No UI element may render in a default sans-serif or serif browser font.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R54]", + "detail": null + }, + { + "local_id": 82, + "plane": "intent", + "kind": "requirement", + "title": "The Connections section of the detail panel for an impasse hub node must render: (1) a CONFLICTING INPUTS group listing nodes via 'conflict…", + "body": "The Connections section of the detail panel for an impasse hub node must render: (1) a CONFLICTING INPUTS group listing nodes via 'conflicting_input' edges with their review status indicator; (2) a RESOLVED BY group showing nodes via 'resolved_by' edges; (3) a SPAWNED group listing child impasses via 'spawned' edges; (4) a REFINED TO group showing the refined impasse via 'refined_to' edges; (5) a status banner indicating whether the impasse is currently unresolved (no resolved_by edges) or resolved. Unresolved impasses must show a pulsing amber 'UNRESOLVED' badge in the Identity section.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R35]", + "detail": null + }, + { + "local_id": 83, + "plane": "intent", + "kind": "criterion", + "title": "For a decision hub node, the Connections section must render five distinct groups: RATIONALE (prose text with phosphor-amber left border),…", + "body": "For a decision hub node, the Connections section must render five distinct groups: RATIONALE (prose text with phosphor-amber left border), CONSIDERED (pills for nodes via 'considered' edges), SELECTED (green-glow pills for nodes via 'selected' edges), REJECTED (dimmed red-indicator pills for nodes via 'rejected' edges), and CONSEQUENCES (pills for nodes via 'consequence' or 'produced' edges). Each pill must be clickable and navigate the detail panel to the referenced node. A 'Trace to grounding' button must be present. Verified against a known decision hub node (e.g., DEC22) from the reference artifact.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR45]", + "detail": null + }, + { + "local_id": 84, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers that keyboard navigation covers only panel controls (Escape closes detail panel, Tab/Shift-Tab moves between UI con…", + "body": "The stakeholder prefers that keyboard navigation covers only panel controls (Escape closes detail panel, Tab/Shift-Tab moves between UI controls, Enter confirms selection); the Sigma.js canvas graph interaction is mouse/touch only.", + "basis": "explicit", + "source": "stakeholder [X28]", + "detail": null + }, + { + "local_id": 85, + "plane": "intent", + "kind": "term", + "title": "The lifecycle of a node represents its current standing in the knowledge graph.", + "body": null, + "basis": "explicit", + "source": "technical-observed [T4]", + "detail": { + "definition": "The lifecycle of a node represents its current standing in the knowledge graph. The four defined lifecycle values are: candidate, active, archived, and withdrawn." + } + }, + { + "local_id": 86, + "plane": "intent", + "kind": "requirement", + "title": "A CSS scanline texture overlay must sit above the WebGL canvas at all times, implemented as a repeating-linear-gradient pseudo-element with…", + "body": "A CSS scanline texture overlay must sit above the WebGL canvas at all times, implemented as a repeating-linear-gradient pseudo-element with pointer-events:none, so it does not intercept canvas mouse/touch events.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R16]", + "detail": null + }, + { + "local_id": 87, + "plane": "intent", + "kind": "context", + "title": "Impasse hub nodes receive a dedicated rendering mode in the detail panel.", + "body": "Impasse hub nodes receive a dedicated rendering mode in the detail panel. The Connections section shows: (1) a 'CONFLICTING INPUTS' group listing nodes connected by 'conflicting_input' edges, each shown as a clickable pill with their review status indicator; (2) a 'RESOLVED BY' group showing nodes connected by 'resolved_by' edges (perspective or decision nodes); (3) a 'SPAWNED' group listing child impasses via 'spawned' edges; (4) a 'REFINED TO' group showing the refined impasse via 'refined_to' edges. A status banner at the top of the Connections section shows whether the impasse is currently unresolved (no resolved_by edges) or resolved. Unresolved impasses are visually flagged with a pulsing amber 'UNRESOLVED' badge in the Identity section. In the micro graph, impasse nodes render with a distinctive hexagonal shape and warning-amber glow.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D24]", + "detail": null + }, + { + "local_id": 88, + "plane": "intent", + "kind": "criterion", + "title": "In the macro timeline, any frame card associated with a perspective hub node (hubType='perspective') must display a small purple indicator…", + "body": "In the macro timeline, any frame card associated with a perspective hub node (hubType='perspective') must display a small purple indicator badge on the card. In the reference artifact, any rederive frame whose derivation produced perspective hub nodes must show this badge. Verified by identifying perspective hub nodes in the reference nodes.json, tracing their frameId, and confirming the badge appears on the corresponding frame card in the macro view.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR95]", + "detail": null + }, + { + "local_id": 89, + "plane": "intent", + "kind": "context", + "title": "The artifact includes a validation report.", + "body": "The artifact includes a validation report. The UI must integrate this data to show which nodes have issues.", + "basis": "explicit", + "source": "stakeholder [X37]", + "detail": null + }, + { + "local_id": 90, + "plane": "intent", + "kind": "criterion", + "title": "Nodes that have one or more validation errors touching their incident edges must render in the micro-view graph with a red-tinted glow halo…", + "body": "Nodes that have one or more validation errors touching their incident edges must render in the micro-view graph with a red-tinted glow halo visually overlaid on their normal phase-color glow. Selecting a known errored edge in the reference artifact's validation.json, identifying its source and target nodes, and visually inspecting those nodes in the graph must confirm the red halo is present and absent on a clean neighboring node.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR22]", + "detail": null + }, + { + "local_id": 91, + "plane": "intent", + "kind": "criterion", + "title": "Given the user clicks a file-picker trigger on the landing page and selects a valid artifact.json, the application must load and parse the…", + "body": "Given the user clicks a file-picker trigger on the landing page and selects a valid artifact.json, the application must load and parse the file identically to drag-and-drop, transitioning to the main explorer view with no server upload.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR2]", + "detail": null + }, + { + "local_id": 92, + "plane": "intent", + "kind": "criterion", + "title": "The left sidebar filter panel must contain exactly the following controls: (1) a text input for full-text search; (2) four phase filter chi…", + "body": "The left sidebar filter panel must contain exactly the following controls: (1) a text input for full-text search; (2) four phase filter chips (grounding, shaping, pinning, defining_done); (3) ten semantic role checkboxes (goal, term, context, constraint, evidence, design, alternative, requirement, criterion, risk); (4) a hub type toggle with options all/decision/justification/impasse/perspective; (5) four epistemic status chips (observed, asserted, assumed, inferred); (6) four authority chips (stakeholder, technical, external, derived); (7) three lifecycle visibility toggles (archived, candidate, withdrawn). All controls must be present in the DOM simultaneously.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR34]", + "detail": null + }, + { + "local_id": 93, + "plane": "intent", + "kind": "requirement", + "title": "The provenance mini-graph must use the same Sigma WebGL program class (node shader, color palette, glow style) as the main micro-view graph…", + "body": "The provenance mini-graph must use the same Sigma WebGL program class (node shader, color palette, glow style) as the main micro-view graph, ensuring visual coherence between the two Sigma instances.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R38]", + "detail": null + }, + { + "local_id": 94, + "plane": "intent", + "kind": "criterion", + "title": "The comparison overlay must include a 'View in graph' button.", + "body": "The comparison overlay must include a 'View in graph' button. Clicking it must: close the comparison overlay, switch to Micro view if Macro view is active, set selectedNodeId to the baseline node's id in the Zustand store, and pan/zoom the Sigma canvas to bring the baseline node into view. The detail panel must open for the baseline node.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR67]", + "detail": null + }, + { + "local_id": 95, + "plane": "intent", + "kind": "criterion", + "title": "Each frame card in the macro timeline must display one intervention annotation chip per intervention record associated with that frameId.", + "body": "Each frame card in the macro timeline must display one intervention annotation chip per intervention record associated with that frameId. Each chip must show the intervention kind (e.g. 'accept_candidate') and a count of targetNodeIds. For the reference artifact: frame 10f07753 must show 3 chips (interventions 0f60db54, 158ac3c4, 926c3761), and frame b40fd568 must show 1 chip (intervention 610c95d1). Hovering a chip must display a tooltip listing targetNodeIds as human-readable displayIds.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR61]", + "detail": null + }, + { + "local_id": 96, + "plane": "intent", + "kind": "criterion", + "title": "For an impasse hub node, the Connections section must render: CONFLICTING INPUTS group (nodes via 'conflicting_input' edges, each showing r…", + "body": "For an impasse hub node, the Connections section must render: CONFLICTING INPUTS group (nodes via 'conflicting_input' edges, each showing review status), RESOLVED BY group (nodes via 'resolved_by' edges), SPAWNED group (child impasses via 'spawned' edges), REFINED TO group (via 'refined_to' edges), and a status banner indicating resolved or unresolved state. An unresolved impasse must show a pulsing amber 'UNRESOLVED' badge in the Identity section. Verified against the trigger impasse node in the reference artifact.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR47]", + "detail": null + }, + { + "local_id": 97, + "plane": "intent", + "kind": "criterion", + "title": "For a content node, the Identity section must display: the full node text, displayId badge, phase badge, lifecycle badge, review status ind…", + "body": "For a content node, the Identity section must display: the full node text, displayId badge, phase badge, lifecycle badge, review status indicator (one of clean/suspect/conditional), semanticRole, epistemicStatus, and authority. For a hub node, the Identity section must display hubType instead of semanticRole/epistemicStatus/authority. Verified by mounting the detail panel for one known content node and one known hub node from the reference artifact and asserting each field's presence and correct value.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR42]", + "detail": null + }, + { + "local_id": 98, + "plane": "intent", + "kind": "context", + "title": "Edge categories (support, workflow, structural) must be visually distinguished using different line styles or colors in the graph visualiza…", + "body": "Edge categories (support, workflow, structural) must be visually distinguished using different line styles or colors in the graph visualization.", + "basis": "explicit", + "source": "stakeholder [X34]", + "detail": null + }, + { + "local_id": 99, + "plane": "intent", + "kind": "constraint", + "title": "The UI cannot generate per-frame LLM summaries at runtime.", + "body": "The UI cannot generate per-frame LLM summaries at runtime. It can only consume summaries that are pre-generated and present in the artifact.", + "basis": "explicit", + "source": "stakeholder [C9]", + "detail": null + }, + { + "local_id": 100, + "plane": "intent", + "kind": "goal", + "title": "The app must present the knowledge graph as a rich, browsable, searchable interface supporting graph visualization, node detail inspection,…", + "body": "The app must present the knowledge graph as a rich, browsable, searchable interface supporting graph visualization, node detail inspection, provenance tracing, decision exploration, filtering, search, and side-by-side baseline/candidate comparison.", + "basis": "explicit", + "source": "stakeholder [G4]", + "detail": null + }, + { + "local_id": 101, + "plane": "intent", + "kind": "term", + "title": "Every content node in the domain model carries three orthogonal classification…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T8]", + "detail": { + "definition": "Every content node in the domain model carries three orthogonal classification axes: semantic role, epistemic status, and authority." + } + }, + { + "local_id": 102, + "plane": "intent", + "kind": "criterion", + "title": "When a lifecycle visibility toggle (archived, candidate, or withdrawn) is switched off, affected nodes must be hidden from the Sigma canvas…", + "body": "When a lifecycle visibility toggle (archived, candidate, or withdrawn) is switched off, affected nodes must be hidden from the Sigma canvas by setting the graphology node attribute 'hidden' to true — not by calling graph.dropNode() or rebuilding the graphology instance. Switching the toggle back on must restore those nodes by setting 'hidden' to false. Active nodes must have no toggle and must always be visible.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR31]", + "detail": null + }, + { + "local_id": 103, + "plane": "intent", + "kind": "context", + "title": "Decision nodes are described by the stakeholder as the most important hub type in the system.", + "body": "Decision nodes are described by the stakeholder as the most important hub type in the system.", + "basis": "explicit", + "source": "stakeholder [X10]", + "detail": null + }, + { + "local_id": 104, + "plane": "intent", + "kind": "criterion", + "title": "The TypeScript type NodeRecord in src/types/artifact.ts must be a discriminated union on the 'kind' field with exactly two variants: one fo…", + "body": "The TypeScript type NodeRecord in src/types/artifact.ts must be a discriminated union on the 'kind' field with exactly two variants: one for kind='content' (including semanticRole, epistemicStatus, authority) and one for kind='hub' (including hubType, rationale). FrameRecord must declare a summary field typed as string | null. The file must import nothing from the spec-elicitation package. Verified by TypeScript compiler with zero type errors.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR33]", + "detail": null + }, + { + "local_id": 105, + "plane": "intent", + "kind": "criterion", + "title": "When the bundler script processes frames.json entries that have no summary field, the resulting artifact.json must include summary: null on…", + "body": "When the bundler script processes frames.json entries that have no summary field, the resulting artifact.json must include summary: null on each such FrameRecord. The TypeScript type for FrameRecord in src/types/artifact.ts must declare summary as string | null, ensuring the UI type-checks without error against both null (current state) and a populated string (future state). Verified by running the bundler on the reference artifact and asserting summary is null on all four frame records.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR76]", + "detail": null + }, + { + "local_id": 106, + "plane": "intent", + "kind": "requirement", + "title": "The baseline/candidate comparison view must be triggerable from two entry points: (1) clicking a fan-in record entry in the macro view; (2)…", + "body": "The baseline/candidate comparison view must be triggerable from two entry points: (1) clicking a fan-in record entry in the macro view; (2) clicking a 'Compare' button in the detail panel of a node with lifecycle=candidate.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R49]", + "detail": null + }, + { + "local_id": 107, + "plane": "intent", + "kind": "context", + "title": "The tech stack for the explorer UI is confirmed as Vite, React, and Tailwind CSS.", + "body": "The tech stack for the explorer UI is confirmed as Vite, React, and Tailwind CSS.", + "basis": "explicit", + "source": "stakeholder [X14]", + "detail": null + }, + { + "local_id": 108, + "plane": "intent", + "kind": "requirement", + "title": "The comparison view must include a 'View in graph' action that focuses the main Sigma canvas on the baseline node, closing the comparison o…", + "body": "The comparison view must include a 'View in graph' action that focuses the main Sigma canvas on the baseline node, closing the comparison overlay and selecting that node in the main graph.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R52]", + "detail": null + }, + { + "local_id": 109, + "plane": "intent", + "kind": "requirement", + "title": "When any filter or search is active, matching nodes must be rendered at full glow intensity and non-matching nodes must be rendered at appr…", + "body": "When any filter or search is active, matching nodes must be rendered at full glow intensity and non-matching nodes must be rendered at approximately 15% opacity in the Sigma canvas. Edges where both endpoints are non-matching must also be dimmed. Graph topology must be preserved — no nodes or edges may be removed from the canvas during filtering.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R27]", + "detail": null + }, + { + "local_id": 110, + "plane": "intent", + "kind": "criterion", + "title": "After loading the smoke-webhook reference artifact, the nodeIndex Map must contain exactly 761 entries (376 active + 88 archived + 288 cand…", + "body": "After loading the smoke-webhook reference artifact, the nodeIndex Map must contain exactly 761 entries (376 active + 88 archived + 288 candidate + 9 withdrawn). The edgeIndex Map must contain exactly 2,662 entries. The frameIndex must contain exactly 4 entries. These counts must match the totals in validation.json (totalNodes=761, totalEdges=2662, totalFrames=4). Any mismatch must be surfaced as a diagnostic warning in the console.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR85]", + "detail": null + }, + { + "local_id": 111, + "plane": "intent", + "kind": "context", + "title": "The micro view is a lineage-focused subgraph showing the spec at the current point in time, with inactive nodes grayed out, and includes a…", + "body": "The micro view is a lineage-focused subgraph showing the spec at the current point in time, with inactive nodes grayed out, and includes a snapshot selector (dropdown or slider) for scrubbing through revisions.", + "basis": "explicit", + "source": "stakeholder [X20]", + "detail": null + }, + { + "local_id": 112, + "plane": "intent", + "kind": "requirement", + "title": "When no node is selected, the right detail panel must be collapsed and the central canvas must expand to occupy the full remaining width af…", + "body": "When no node is selected, the right detail panel must be collapsed and the central canvas must expand to occupy the full remaining width after the left sidebar.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R9]", + "detail": null + }, + { + "local_id": 113, + "plane": "intent", + "kind": "requirement", + "title": "Clicking a frame card in the macro timeline must open a modal node-diff list showing which nodes changed in that frame.", + "body": "Clicking a frame card in the macro timeline must open a modal node-diff list showing which nodes changed in that frame. The full zoom-into-frame WebGL subgraph transition is explicitly deferred; the modal diff list is the required behavior for the current iteration.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R48]", + "detail": null + }, + { + "local_id": 114, + "plane": "intent", + "kind": "requirement", + "title": "The application must accept an optional ?artifact= query parameter; when present it must fetch artifact.json via fetch() from that URL…", + "body": "The application must accept an optional ?artifact= query parameter; when present it must fetch artifact.json via fetch() from that URL and bypass the drop zone, enabling remote sharing without user file selection.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R2]", + "detail": null + }, + { + "local_id": 115, + "plane": "intent", + "kind": "context", + "title": "There is a potential conflict between the preference for browser File API loading (local filesystem drop zone) and the requirement that loa…", + "body": "There is a potential conflict between the preference for browser File API loading (local filesystem drop zone) and the requirement that loading also work when the app is hosted remotely. These two approaches may require different loading mechanisms.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK6]", + "detail": null + }, + { + "local_id": 116, + "plane": "intent", + "kind": "requirement", + "title": "The application must parse artifact.json as a single bundled file with the structure: { manifest, sources, extractedClaims, interventions,…", + "body": "The application must parse artifact.json as a single bundled file with the structure: { manifest, sources, extractedClaims, interventions, graph: { nodes, edges, frames, derivationRuns, fanInRecords, snapshots }, reports: { validation } }. Any artifact.json missing a required top-level key must produce a CRT-themed error state, not a crash or raw unstyled error.", + "basis": "accepted_review_set", + "source": "technical-inferred [R3]", + "detail": null + }, + { + "local_id": 117, + "plane": "intent", + "kind": "context", + "title": "The spec elicitation system is an experimental pipeline within Kael that takes conversational input (interview transcripts, context documen…", + "body": "The spec elicitation system is an experimental pipeline within Kael that takes conversational input (interview transcripts, context documents) and produces a structured specification as a knowledge graph.", + "basis": "explicit", + "source": "external-observed [X3]", + "detail": null + }, + { + "local_id": 118, + "plane": "intent", + "kind": "context", + "title": "Decision hub nodes receive a dedicated rendering mode in the detail panel's Connections section (X35, X10).", + "body": "Decision hub nodes receive a dedicated rendering mode in the detail panel's Connections section (X35, X10). The section renders: (1) a 'RATIONALE' block showing the decision's rationale prose in a styled blockquote with phosphor-amber left border; (2) a 'CONSIDERED' group listing all nodes connected by 'considered' edges, shown as clickable displayId pills; (3) a 'SELECTED' group with a green glow indicator showing the chosen alternative(s) via 'selected' edges; (4) a 'REJECTED' group with a dimmed red indicator showing rejected alternatives via 'rejected' edges; (5) a 'CONSEQUENCES' group listing nodes connected by 'consequence' or 'produced' edges. Each pill in these groups is clickable, navigating the detail panel to that node. A 'Trace to grounding' button traverses the support edges from the decision's considered nodes back to grounding-phase nodes and highlights that subgraph in the main Sigma canvas.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D23]", + "detail": null + }, + { + "local_id": 119, + "plane": "intent", + "kind": "criterion", + "title": "When artifact.json is missing any required top-level key (manifest, sources, extractedClaims, interventions, graph, reports), the applicati…", + "body": "When artifact.json is missing any required top-level key (manifest, sources, extractedClaims, interventions, graph, reports), the application must display a CRT-themed error state with a legible error message. No JavaScript exception may propagate to a blank screen or default browser error UI.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR4]", + "detail": null + }, + { + "local_id": 120, + "plane": "intent", + "kind": "context", + "title": "The macro view surfaces the regression/recovery narrative (X36) through explicit visual encoding of frame relationships: (1) The initial fr…", + "body": "The macro view surfaces the regression/recovery narrative (X36) through explicit visual encoding of frame relationships: (1) The initial frame trunk is rendered in phosphor-green as the primary timeline spine. (2) Rederive frames branch rightward and are rendered in phosphor-amber, with their connecting edge labeled with the triggerImpasseId (shown as a displayId badge). (3) Fan-in record edges connecting rederive frames back to the trunk are rendered in a brighter green with an arrow labeled 'RECONCILED'. (4) Impasse nodes referenced by triggerImpasseIds are shown as warning-colored hexagonal badges on the branch edges. (5) Perspective hub nodes (from the CSP model, X11) are shown as small purple indicator badges on their associated frame cards. (6) The nudgingActive flag on a rederive frame is shown as a 'NUDGED' indicator badge. Together these elements make the full impasse→rederive→fan-out→reconciliation cycle legible at a glance.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D18]", + "detail": null + }, + { + "local_id": 121, + "plane": "intent", + "kind": "context", + "title": "The macro view shows the different frames and how they relate over time, including per-frame LLM-generated summaries, lines connecting fram…", + "body": "The macro view shows the different frames and how they relate over time, including per-frame LLM-generated summaries, lines connecting frames representing impasses/perspectives/derivation relationships, and the ability to zoom into a single frame to see which nodes changed.", + "basis": "explicit", + "source": "stakeholder [X21]", + "detail": null + }, + { + "local_id": 122, + "plane": "intent", + "kind": "requirement", + "title": "Impasse hub nodes must render with a distinctive hexagonal shape in the micro-view Sigma graph, in addition to their warning-amber glow, ma…", + "body": "Impasse hub nodes must render with a distinctive hexagonal shape in the micro-view Sigma graph, in addition to their warning-amber glow, making them visually distinguishable from other hub node types at a glance.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R60]", + "detail": null + }, + { + "local_id": 123, + "plane": "intent", + "kind": "criterion", + "title": "In the macro timeline, edges between frames must use distinct visual encoding by type: trunk-to-rederive-branch edges (triggered by an impa…", + "body": "In the macro timeline, edges between frames must use distinct visual encoding by type: trunk-to-rederive-branch edges (triggered by an impasse) must be drawn in warning amber and labeled with the triggerImpasseId rendered as a displayId badge; fan-in record edges reconnecting rederive frames to the baseline must be drawn in bright green and labeled 'RECONCILED'. Verified by inspecting the rendered color and label of each edge in the reference artifact's macro timeline.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR59]", + "detail": null + }, + { + "local_id": 124, + "plane": "intent", + "kind": "constraint", + "title": "Authentication and multi-user features are out of scope.", + "body": "Authentication and multi-user features are out of scope.", + "basis": "explicit", + "source": "stakeholder [C5]", + "detail": null + }, + { + "local_id": 125, + "plane": "intent", + "kind": "context", + "title": "There is a tension between loading a single combined artifact.json (stakeholder preference) and loading individual artifact files from a di…", + "body": "There is a tension between loading a single combined artifact.json (stakeholder preference) and loading individual artifact files from a directory path or URL prefix (also a stated requirement). These two loading models may need reconciliation.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK7]", + "detail": null + }, + { + "local_id": 126, + "plane": "intent", + "kind": "context", + "title": "At load time the UI builds an in-memory graph store from artifact.json using a flat index structure: nodeIndex (Map), edgeIndex (…", + "body": "At load time the UI builds an in-memory graph store from artifact.json using a flat index structure: nodeIndex (Map), edgeIndex (Map), adjacency (Map), frameIndex (Map), snapshotIndex (Map), and derivedIndex (Map) built by joining validation.json errors to their source/target nodes. Lifecycle filter state is maintained as a reactive set of visible lifecycle values. The active node set for the micro view is derived from the selected snapshot's activeNodeIds array. All indexes are built once on load; no re-parsing occurs during session.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D5]", + "detail": null + }, + { + "local_id": 127, + "plane": "intent", + "kind": "requirement", + "title": "In the micro-view graph, edge color must visually distinguish the three edge categories: support edges (derived_from, depends_on, informed_…", + "body": "In the micro-view graph, edge color must visually distinguish the three edge categories: support edges (derived_from, depends_on, informed_by) in dim amber; workflow edges (produced, considered, selected, rejected, consequence, conflicting_input, resolved_by, spawned, refined_to, aggregates) in brighter green; structural edges (conflicts_with, motivates, defines, references, satisfied_by, operationalized_by, alternative_to, revises_baseline, and all lineage edges) in muted cyan.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R13]", + "detail": null + }, + { + "local_id": 128, + "plane": "intent", + "kind": "requirement", + "title": "The comparison view must render as a split overlay that temporarily replaces or expands the right detail panel.", + "body": "The comparison view must render as a split overlay that temporarily replaces or expands the right detail panel. The left column must show the baseline node and the right column must show the candidate node. Differences in text, semantic role, epistemic status, and authority must be highlighted using a line-diff style with phosphor-colored additions and deletions.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R50]", + "detail": null + }, + { + "local_id": 129, + "plane": "intent", + "kind": "goal", + "title": "The app must operate entirely against statically loaded JSON artifact files with no backend, and must be deployable as a static site.", + "body": "The app must operate entirely against statically loaded JSON artifact files with no backend, and must be deployable as a static site.", + "basis": "explicit", + "source": "stakeholder [G5]", + "detail": null + }, + { + "local_id": 130, + "plane": "intent", + "kind": "criterion", + "title": "When a node is selected, all three layout regions must be simultaneously visible: left sidebar (filter/search/results), central canvas, and…", + "body": "When a node is selected, all three layout regions must be simultaneously visible: left sidebar (filter/search/results), central canvas, and right detail panel. Measuring computed widths of all three regions must return values greater than zero. No region may be hidden, collapsed, or overlaid by another during normal selected-node state.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR9]", + "detail": null + }, + { + "local_id": 131, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers building the macro timeline view using WebGL so that future zoom-into-frame functionality is naturally achievable.", + "body": "The stakeholder prefers building the macro timeline view using WebGL so that future zoom-into-frame functionality is naturally achievable.", + "basis": "explicit", + "source": "stakeholder [X29]", + "detail": null + }, + { + "local_id": 132, + "plane": "intent", + "kind": "requirement", + "title": "Pressing the Escape key must close the right detail panel and clear the current node selection.", + "body": "Pressing the Escape key must close the right detail panel and clear the current node selection. If a comparison overlay is open, Escape must close the comparison overlay and return to the detail panel rather than closing the detail panel.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R33]", + "detail": null + }, + { + "local_id": 133, + "plane": "intent", + "kind": "term", + "title": "A fan-in record captures the result of the reconciliation step where candidate…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T14]", + "detail": { + "definition": "A fan-in record captures the result of the reconciliation step where candidate branches are merged back into the active baseline after a fan-out/clean-room re-derivation cycle." + } + }, + { + "local_id": 134, + "plane": "intent", + "kind": "criterion", + "title": "Clicking any clickable displayId pill within the Connections section (in any of the decision, impasse, or justification group rows) must up…", + "body": "Clicking any clickable displayId pill within the Connections section (in any of the decision, impasse, or justification group rows) must update selectedNodeId in the Zustand store to the referenced node's id, causing the detail panel to re-render for that node and the main Sigma canvas selection highlight to move to that node. The panel history must allow the user to return to the previously selected node via browser back or a dedicated back control if provided.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR86]", + "detail": null + }, + { + "local_id": 135, + "plane": "intent", + "kind": "context", + "title": "Per-frame LLM summaries will be pre-generated by the elicitation pipeline during artifact bundling and stored in FrameRecord or a companion…", + "body": "Per-frame LLM summaries will be pre-generated by the elicitation pipeline during artifact bundling and stored in FrameRecord or a companion structure within artifact.json; they are not generated at runtime by the UI.", + "basis": "explicit", + "source": "stakeholder [X23]", + "detail": null + }, + { + "local_id": 136, + "plane": "intent", + "kind": "criterion", + "title": "Clicking any node in the provenance mini-graph must update selectedNodeId in the Zustand store to that node's id, causing the main detail p…", + "body": "Clicking any node in the provenance mini-graph must update selectedNodeId in the Zustand store to that node's id, causing the main detail panel to re-render for the clicked node and the main Sigma canvas selection to update accordingly. The mini-graph must then re-render to show the new node's upstream subgraph.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR52]", + "detail": null + }, + { + "local_id": 137, + "plane": "intent", + "kind": "goal", + "title": "The system must enable users to interactively explore a spec elicitation artifact as a read-only single-page web application.", + "body": "The system must enable users to interactively explore a spec elicitation artifact as a read-only single-page web application.", + "basis": "explicit", + "source": "stakeholder [G1]", + "detail": null + }, + { + "local_id": 138, + "plane": "intent", + "kind": "requirement", + "title": "The toolbar must contain lifecycle visibility toggles for archived, candidate, and withdrawn nodes.", + "body": "The toolbar must contain lifecycle visibility toggles for archived, candidate, and withdrawn nodes. Active nodes must always be visible and cannot be toggled off. When a lifecycle toggle is changed, node visibility must be updated via Sigma's node attribute API (setting hidden=true/false) rather than rebuilding the graphology graph.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R22]", + "detail": null + }, + { + "local_id": 139, + "plane": "intent", + "kind": "context", + "title": "The perspective hub is modeled as a constraint satisfaction problem with axes, alternatives, constraints, and guarded impasses; perspective…", + "body": "The perspective hub is modeled as a constraint satisfaction problem with axes, alternatives, constraints, and guarded impasses; perspectives are a presentation layer derived from the CSP solver, not the primary semantic unit.", + "basis": "explicit", + "source": "technical-observed [X11]", + "detail": null + }, + { + "local_id": 140, + "plane": "intent", + "kind": "constraint", + "title": "The file-loading mechanism must work when the app is hosted remotely.", + "body": "The file-loading mechanism must work when the app is hosted remotely. The app must not directly serve artifact files, because the website may be hosted remotely in the future.", + "basis": "explicit", + "source": "stakeholder [C6]", + "detail": null + }, + { + "local_id": 141, + "plane": "intent", + "kind": "criterion", + "title": "When multiple filter dimensions are active simultaneously (e.g., phase=shaping AND semanticRole=design AND authority=derived), the results…", + "body": "When multiple filter dimensions are active simultaneously (e.g., phase=shaping AND semanticRole=design AND authority=derived), the results list must contain only nodes satisfying all active conditions. Enabling a second filter must never increase the result count. Verified by: activating two mutually constraining filters against the reference artifact and asserting the result set is the mathematical intersection of each filter applied individually.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR35]", + "detail": null + }, + { + "local_id": 142, + "plane": "intent", + "kind": "requirement", + "title": "All interactive HTML elements must use visible focus rings styled in phosphor-amber, ensuring keyboard focus is always visible on the dark…", + "body": "All interactive HTML elements must use visible focus rings styled in phosphor-amber, ensuring keyboard focus is always visible on the dark background.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R58]", + "detail": null + }, + { + "local_id": 143, + "plane": "oracle", + "kind": "evidence", + "title": "The confirmed artifact file layout is: graph/ subdirectory containing nodes.json, edges.json, frames.json, derivation-runs.json, fan-in-rec…", + "body": "The confirmed artifact file layout is: graph/ subdirectory containing nodes.json, edges.json, frames.json, derivation-runs.json, fan-in-records.json, and snapshots.json; top-level containing manifest.json, sources.json, extracted-claims.json, and interventions.json.", + "basis": "explicit", + "source": "technical-observed [E4]", + "detail": null + }, + { + "local_id": 144, + "plane": "intent", + "kind": "criterion", + "title": "When a frame card's summary field is null (as is the case for all frames in the current reference artifact), the summary region of the fram…", + "body": "When a frame card's summary field is null (as is the case for all frames in the current reference artifact), the summary region of the frame card must display a muted placeholder text 'NO SUMMARY AVAILABLE' in dimmed monospace style. No JavaScript error, broken layout, or missing DOM element may result from a null summary. When a summary string is present, it must be rendered in its place without any code change.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR58]", + "detail": null + }, + { + "local_id": 145, + "plane": "intent", + "kind": "term", + "title": "Authority identifies the source type of a node's claim.", + "body": null, + "basis": "explicit", + "source": "technical-observed [T6]", + "detail": { + "definition": "Authority identifies the source type of a node's claim. The four defined values are: stakeholder, technical, external, and derived." + } + }, + { + "local_id": 146, + "plane": "intent", + "kind": "requirement", + "title": "The right detail panel must have four collapsible sections rendered top-to-bottom: (1) Identity — always expanded by default, showing full…", + "body": "The right detail panel must have four collapsible sections rendered top-to-bottom: (1) Identity — always expanded by default, showing full node text, displayId badge, phase badge, lifecycle badge, review status indicator, and kind-specific classification fields; (2) Connections — hub-type-specific relationship tables; (3) Provenance — embedded Sigma.js mini-graph; (4) Validation — shown only when review status is not clean. The Identity section must always remain visible at the top.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R31]", + "detail": null + }, + { + "local_id": 147, + "plane": "intent", + "kind": "criterion", + "title": "When the results list has focus and a row is highlighted via Arrow key navigation, pressing Enter must select that node: selectedNodeId in…", + "body": "When the results list has focus and a row is highlighted via Arrow key navigation, pressing Enter must select that node: selectedNodeId in the Zustand store must be set to the row's node id, and the right detail panel must open for that node with the flicker animation. Verified by simulating ArrowDown then Enter on the results list and asserting the store update and panel appearance.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR73]", + "detail": null + }, + { + "local_id": 148, + "plane": "intent", + "kind": "criterion", + "title": "When a node with lifecycle='candidate' is selected in the micro-view graph and the detail panel is open, a 'Compare' button must be visible…", + "body": "When a node with lifecycle='candidate' is selected in the micro-view graph and the detail panel is open, a 'Compare' button must be visible in the Identity section or the panel header. Nodes with lifecycle='active', 'archived', or 'withdrawn' must not show this button. Verified by selecting one candidate node and one active node from the reference artifact and asserting button presence/absence in each case.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR92]", + "detail": null + }, + { + "local_id": 149, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers loading artifact.json from the user's local filesystem via the browser File API, with a landing screen presenting a…", + "body": "The stakeholder prefers loading artifact.json from the user's local filesystem via the browser File API, with a landing screen presenting a file drop zone, requiring no server or URL.", + "basis": "explicit", + "source": "stakeholder [X18]", + "detail": null + }, + { + "local_id": 150, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers a CRT-inspired visual design language: a polished aesthetic evoking vintage phosphor displays with amber or green p…", + "body": "The stakeholder prefers a CRT-inspired visual design language: a polished aesthetic evoking vintage phosphor displays with amber or green phosphor colors on dark backgrounds, subtle scanline textures, and gentle CRT glow/bloom effects on interactive elements.", + "basis": "explicit", + "source": "stakeholder [X15]", + "detail": null + }, + { + "local_id": 151, + "plane": "intent", + "kind": "criterion", + "title": "After artifact.json is parsed, all eight in-memory indexes (nodeIndex, edgeIndex, adjacency, frameIndex, snapshotIndex, validationIssuesByE…", + "body": "After artifact.json is parsed, all eight in-memory indexes (nodeIndex, edgeIndex, adjacency, frameIndex, snapshotIndex, validationIssuesByEdgeId, edgeIssuesByNodeId, interventionsByNodeId) must be fully populated before the main explorer UI renders. No index build or re-parse operation may be triggered by user interaction after this initial pass. Verified by: instrumenting the store initializer and asserting all Maps are non-empty after load with zero subsequent re-build calls during a full interaction session.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR24]", + "detail": null + }, + { + "local_id": 152, + "plane": "intent", + "kind": "requirement", + "title": "Application state must be managed in a single Zustand store containing: loadedArtifact, all derived indexes, activeView, selectedNodeId, se…", + "body": "Application state must be managed in a single Zustand store containing: loadedArtifact, all derived indexes, activeView, selectedNodeId, selectedSnapshotRevision, filterState, and comparisonState. React components must subscribe to fine-grained store slices to prevent unnecessary re-renders during filter and hover interactions.", + "basis": "accepted_review_set", + "source": "technical-inferred [R23]", + "detail": null + }, + { + "local_id": 153, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers: a detail panel with CRT power-on flicker animation (~150ms) rather than slide-in; collapsible sections with most i…", + "body": "The stakeholder prefers: a detail panel with CRT power-on flicker animation (~150ms) rather than slide-in; collapsible sections with most important information always visible at top; top section showing full node text, displayId, phase badge, lifecycle badge, and review status indicator.", + "basis": "explicit", + "source": "stakeholder [X24]", + "detail": null + }, + { + "local_id": 154, + "plane": "intent", + "kind": "criterion", + "title": "Each boundary between the three layout regions must have a visible drag handle.", + "body": "Each boundary between the three layout regions must have a visible drag handle. Dragging a handle must resize the adjacent panels proportionally in real time, with both panels maintaining a non-zero minimum width throughout the drag. After release, the new widths must persist for the remainder of the session.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR10]", + "detail": null + }, + { + "local_id": 155, + "plane": "intent", + "kind": "criterion", + "title": "For the reference artifact, the single snapshot at revision 4 lists all four frameIds in its frameIds array.", + "body": "For the reference artifact, the single snapshot at revision 4 lists all four frameIds in its frameIds array. When the snapshot slider is set to revision 4, the active node set must be derived from that snapshot's activeNodeIds array (376 active nodes). The status line below the slider must display revision 4 and all four frameId values (or their display equivalents). Verified by loading the reference artifact and reading the status line content.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR91]", + "detail": null + }, + { + "local_id": 156, + "plane": "intent", + "kind": "context", + "title": "The macro timeline is laid out as a vertical timeline showing one narrative from top to bottom, branching out horizontally at derivation lo…", + "body": "The macro timeline is laid out as a vertical timeline showing one narrative from top to bottom, branching out horizontally at derivation loops.", + "basis": "explicit", + "source": "stakeholder [X22]", + "detail": null + }, + { + "local_id": 157, + "plane": "intent", + "kind": "criterion", + "title": "Every interactive HTML element (buttons, filter chips, panel headers, results list rows) must have a visually distinct hover state that int…", + "body": "Every interactive HTML element (buttons, filter chips, panel headers, results list rows) must have a visually distinct hover state that intensifies glow via CSS transition on box-shadow and/or text-shadow. Verified by: programmatically triggering :hover on at least one element of each interactive type and asserting that the computed box-shadow or text-shadow value differs from the non-hovered state. No interactive element may have an identical computed style before and after hover.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR70]", + "detail": null + }, + { + "local_id": 158, + "plane": "intent", + "kind": "criterion", + "title": "The macro timeline must lay out the reference artifact's four frames correctly: the initial frame (mode=initial, id a03f944e) must appear o…", + "body": "The macro timeline must lay out the reference artifact's four frames correctly: the initial frame (mode=initial, id a03f944e) must appear on the main vertical trunk; the three rederive frames (ids 10f07753, b40fd568, b9236ccf, all with parentFrameId=a03f944e) must appear as horizontal siblings branching to the right at the same vertical level as each other, not as a vertical chain. Verified by inspecting the rendered positions of each frame card's center point on the WebGL canvas.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR56]", + "detail": null + }, + { + "local_id": 159, + "plane": "intent", + "kind": "context", + "title": "The provenance mini-graph within the node detail panel is acknowledged to be complex to implement, particularly ensuring it remains visuall…", + "body": "The provenance mini-graph within the node detail panel is acknowledged to be complex to implement, particularly ensuring it remains visually coherent with the main graph.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK3]", + "detail": null + }, + { + "local_id": 160, + "plane": "intent", + "kind": "requirement", + "title": "The comparison view must display the fan-in grouping rationale (from fan-in-records.json groupings[].rationale) as a decision banner betwee…", + "body": "The comparison view must display the fan-in grouping rationale (from fan-in-records.json groupings[].rationale) as a decision banner between the baseline and candidate columns. All nodes in the same fan-in grouping must be accessible via a tab row above the split columns.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R51]", + "detail": null + }, + { + "local_id": 161, + "plane": "intent", + "kind": "context", + "title": "The stakeholder prefers displaying interventions in two places: in the node detail panel (showing which interventions targeted the node) an…", + "body": "The stakeholder prefers displaying interventions in two places: in the node detail panel (showing which interventions targeted the node) and as annotations on frames in the macro timeline.", + "basis": "explicit", + "source": "stakeholder [X26]", + "detail": null + }, + { + "local_id": 162, + "plane": "intent", + "kind": "requirement", + "title": "The loading state, error state, and empty state (no artifact loaded) must each have bespoke CRT-themed treatments.", + "body": "The loading state, error state, and empty state (no artifact loaded) must each have bespoke CRT-themed treatments. No raw unstyled, blank, or default-browser-styled state may appear at any point during the application lifecycle.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R56]", + "detail": null + }, + { + "local_id": 163, + "plane": "intent", + "kind": "requirement", + "title": "The toolbar must display a global validation summary badge showing the total count of validation errors from validation.json.", + "body": "The toolbar must display a global validation summary badge showing the total count of validation errors from validation.json. The badge must pulse in amber when any errors are present.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R40]", + "detail": null + }, + { + "local_id": 164, + "plane": "intent", + "kind": "criterion", + "title": "The production build output (vite build) must consist entirely of static files (HTML, JS, CSS, assets) with no server-side runtime requirem…", + "body": "The production build output (vite build) must consist entirely of static files (HTML, JS, CSS, assets) with no server-side runtime requirement. Serving the dist/ directory from any static file host (e.g., GitHub Pages, S3, Netlify) must produce a fully functional application. Zero fetch() calls to a backend API may occur during normal operation.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR8]", + "detail": null + }, + { + "local_id": 165, + "plane": "intent", + "kind": "context", + "title": "The spec elicitation pipeline uses fan-out/fan-in with clean-room re-derivation to handle contradictions, a perspective hub (CSP model) to…", + "body": "The spec elicitation pipeline uses fan-out/fan-in with clean-room re-derivation to handle contradictions, a perspective hub (CSP model) to present design alternatives, and reconciliation to merge candidates into the active baseline.", + "basis": "explicit", + "source": "external-observed [X4]", + "detail": null + }, + { + "local_id": 166, + "plane": "intent", + "kind": "requirement", + "title": "When a search query is active, a results list must appear in the left sidebar below the filter controls, showing a scrollable list of match…", + "body": "When a search query is active, a results list must appear in the left sidebar below the filter controls, showing a scrollable list of matching nodes sorted by displayId. Each row must show the node's displayId, phase badge, semantic role or hub type badge, and truncated node text. The results list must remain visible simultaneously with the highlighted graph.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R28]", + "detail": null + }, + { + "local_id": 167, + "plane": "intent", + "kind": "term", + "title": "artifact.json is the single bundled output file combining all pipeline output f…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T17]", + "detail": { + "definition": "artifact.json is the single bundled output file combining all pipeline output files, loaded by the explorer UI to provide all graph data, metadata, and reports." + } + }, + { + "local_id": 168, + "plane": "intent", + "kind": "criterion", + "title": "A user who opens the application for the first time in a browser with no query parameters must be presented with the drop zone landing scre…", + "body": "A user who opens the application for the first time in a browser with no query parameters must be presented with the drop zone landing screen immediately, with no configuration dialogs, login prompts, URL entry fields, or setup steps. The drop zone must be the sole interactive element required to load an artifact. Verified by loading the app with no query params and asserting only the drop zone and optional file-picker button are the primary interactive elements.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR93]", + "detail": null + }, + { + "local_id": 169, + "plane": "intent", + "kind": "criterion", + "title": "In the macro timeline, the initial frame card (mode=initial, id a03f944e) and the main trunk line connecting it must be rendered in phospho…", + "body": "In the macro timeline, the initial frame card (mode=initial, id a03f944e) and the main trunk line connecting it must be rendered in phosphor-green (#39FF14 or the defined phosphor-green token). Rederive frame cards must be rendered in phosphor-amber (#FFB000). Verified by sampling the rendered WebGL pixel color at the center of the initial frame card and at the center of one rederive frame card and comparing against the defined theme token values.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR88]", + "detail": null + }, + { + "local_id": 170, + "plane": "intent", + "kind": "context", + "title": "Each intervention record in interventions.json carries: id, frameId, phase, kind (e.g.", + "body": "Each intervention record in interventions.json carries: id, frameId, phase, kind (e.g. accept_candidate), targetNodeIds array, text (nullable), createdAt. Interventions are associated with a frame, not directly with individual nodes — the targetNodeIds array provides the node linkage.", + "basis": "explicit", + "source": "technical-observed [X47]", + "detail": null + }, + { + "local_id": 171, + "plane": "intent", + "kind": "criterion", + "title": "In the micro-view graph, nodes must be visually colored with four distinct phosphor hues corresponding to the four derivation phases: groun…", + "body": "In the micro-view graph, nodes must be visually colored with four distinct phosphor hues corresponding to the four derivation phases: grounding, shaping, pinning, and defining_done. The same four colors must appear on phase badge UI elements in the sidebar results list, the toolbar filter chips, and the detail panel phase badge — verified by comparing computed CSS color values across all locations.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR16]", + "detail": null + }, + { + "local_id": 172, + "plane": "intent", + "kind": "term", + "title": "Epistemic status expresses the evidentiary basis for a node's claim.", + "body": null, + "basis": "explicit", + "source": "technical-observed [T5]", + "detail": { + "definition": "Epistemic status expresses the evidentiary basis for a node's claim. The four defined values are: observed, asserted, assumed, and inferred." + } + }, + { + "local_id": 173, + "plane": "intent", + "kind": "criterion", + "title": "Each row in the search results list must display: the node's displayId, a phase badge styled in the correct phase color, a semantic role ba…", + "body": "Each row in the search results list must display: the node's displayId, a phase badge styled in the correct phase color, a semantic role badge (for content nodes) or hub type badge (for hub nodes), and a truncated version of the node text. Verified by rendering the reference artifact, searching for a known term, and asserting all four elements are present in each result row's DOM.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR38]", + "detail": null + }, + { + "local_id": 174, + "plane": "intent", + "kind": "criterion", + "title": "When the user switches from Micro view to Macro view and back to Micro view, the filter state (active phase chips, role checkboxes, search…", + "body": "When the user switches from Micro view to Macro view and back to Micro view, the filter state (active phase chips, role checkboxes, search query, lifecycle toggles) must be identical to what it was before switching. The Sigma canvas must restore the highlighting/dimming state reflecting the preserved filter. Verified by applying a multi-filter, switching views, and asserting the Zustand filterState is unchanged.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR90]", + "detail": null + }, + { + "local_id": 175, + "plane": "intent", + "kind": "criterion", + "title": "Loading a malformed artifact.json (e.g., a file with a valid JSON structure but missing the 'graph' key) must render a CRT-styled error scr…", + "body": "Loading a malformed artifact.json (e.g., a file with a valid JSON structure but missing the 'graph' key) must render a CRT-styled error screen with a descriptive message identifying the missing key. The error screen must use phosphor-amber or phosphor-text color on a phosphor-dim background, use the monospace font, and must not display any raw browser error dialog or white screen.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR81]", + "detail": null + }, + { + "local_id": 176, + "plane": "intent", + "kind": "criterion", + "title": "Inspecting the Sigma.js canvas element in the DOM must confirm it is a element with a WebGL rendering context (getContext('webgl')…", + "body": "Inspecting the Sigma.js canvas element in the DOM must confirm it is a element with a WebGL rendering context (getContext('webgl') or getContext('webgl2') must return a non-null value). The application must not fall back to SVG or Canvas2D rendering for the micro-view graph.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR14]", + "detail": null + }, + { + "local_id": 177, + "plane": "intent", + "kind": "criterion", + "title": "In the micro-view graph, node opacity must match lifecycle state: active nodes at 100% opacity; candidate nodes at approximately 60% (±5%);…", + "body": "In the micro-view graph, node opacity must match lifecycle state: active nodes at 100% opacity; candidate nodes at approximately 60% (±5%); archived nodes at approximately 20% (±5%); withdrawn nodes at approximately 10% (±5%). Sampling one node of each lifecycle from the reference artifact and measuring the rendered alpha value via the WebGL shader uniform or Sigma attribute must confirm the correct opacity for each.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR20]", + "detail": null + }, + { + "local_id": 178, + "plane": "intent", + "kind": "criterion", + "title": "In the Connections section of the detail panel, a collapsible 'Interventions' sub-section must list all intervention records that reference…", + "body": "In the Connections section of the detail panel, a collapsible 'Interventions' sub-section must list all intervention records that reference the selected node in their targetNodeIds array. Each entry must show: intervention kind, frameId (rendered as a link that activates the macro view focused on that frame), and createdAt timestamp. For a node not referenced by any intervention, the sub-section must either be absent or show an empty state message.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR63]", + "detail": null + }, + { + "local_id": 179, + "plane": "intent", + "kind": "criterion", + "title": "Every text-bearing element in the application — node text, displayIds, data values, filter chips, badge labels, panel headers, results list…", + "body": "Every text-bearing element in the application — node text, displayIds, data values, filter chips, badge labels, panel headers, results list rows, and toolbar controls — must render in a monospaced font (JetBrains Mono or equivalent). Inspecting the computed font-family of a representative sample of 10 distinct element types must return a monospace font in all cases. No element may render in the browser default sans-serif or serif font.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR69]", + "detail": null + }, + { + "local_id": 180, + "plane": "intent", + "kind": "requirement", + "title": "Filter and selection state changes must be debounced at 16ms before triggering a Sigma canvas refresh, preventing per-keystroke re-renders…", + "body": "Filter and selection state changes must be debounced at 16ms before triggering a Sigma canvas refresh, preventing per-keystroke re-renders during text search input.", + "basis": "accepted_review_set", + "source": "technical-inferred [R29]", + "detail": null + }, + { + "local_id": 181, + "plane": "intent", + "kind": "requirement", + "title": "When artifact.json is successfully parsed, the application must transition to the main explorer view with a CRT power-on animation before d…", + "body": "When artifact.json is successfully parsed, the application must transition to the main explorer view with a CRT power-on animation before displaying any graph content. When the app is in the file-drop landing state, no raw unstyled or blank screen may appear.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R4]", + "detail": null + }, + { + "local_id": 182, + "plane": "intent", + "kind": "context", + "title": "A subtle scanline CSS overlay sits above the WebGL canvas to reinforce the CRT aesthetic.", + "body": "A subtle scanline CSS overlay sits above the WebGL canvas to reinforce the CRT aesthetic.", + "basis": "explicit", + "source": "stakeholder [X41]", + "detail": null + }, + { + "local_id": 183, + "plane": "intent", + "kind": "criterion", + "title": "The phase color used for a node's glow in the Sigma micro-view graph must exactly match the color used for that node's phase badge in the d…", + "body": "The phase color used for a node's glow in the Sigma micro-view graph must exactly match the color used for that node's phase badge in the detail panel Identity section, the phase chip in the sidebar filter panel, and the phase badge in the results list row. Extracting the RGB value of each location for a known node (e.g., a grounding-phase node) must return identical values across all four locations.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR82]", + "detail": null + }, + { + "local_id": 184, + "plane": "intent", + "kind": "criterion", + "title": "For a justification hub node, the Connections section must render a PREMISES group (nodes via 'informed_by' edges) and a CONCLUSIONS group…", + "body": "For a justification hub node, the Connections section must render a PREMISES group (nodes via 'informed_by' edges) and a CONCLUSIONS group (nodes via 'produced' edges). Each entry in both groups must be a clickable pill that navigates the detail panel to the referenced node. Verified by mounting the detail panel for a known justification hub node and asserting both groups are present with correct node references.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR48]", + "detail": null + }, + { + "local_id": 185, + "plane": "intent", + "kind": "requirement", + "title": "The micro-view graph must be rendered using Sigma.js v3 with a WebGL backend.", + "body": "The micro-view graph must be rendered using Sigma.js v3 with a WebGL backend. The renderer must support interactive frame rates for the full reference dataset of 761 total nodes and 2,662 edges.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R10]", + "detail": null + }, + { + "local_id": 186, + "plane": "intent", + "kind": "criterion", + "title": "In the micro-view graph, every node with kind='content' must render as a circle and every node with kind='hub' must render as a diamond.", + "body": "In the micro-view graph, every node with kind='content' must render as a circle and every node with kind='hub' must render as a diamond. Sampling at least 20 nodes of each kind from the reference artifact and inspecting their rendered shapes via the Sigma node program must confirm the correct geometry for all sampled nodes.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR17]", + "detail": null + }, + { + "local_id": 187, + "plane": "intent", + "kind": "requirement", + "title": "Nodes in the micro-view graph must be rendered with a per-node phosphor glow implemented as a WebGL fragment shader.", + "body": "Nodes in the micro-view graph must be rendered with a per-node phosphor glow implemented as a WebGL fragment shader. The glow intensity must increase on hover and on selection, driven by shader uniforms updated in response to pointer events.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R15]", + "detail": null + }, + { + "local_id": 188, + "plane": "intent", + "kind": "context", + "title": "A WebGL-based renderer gives less fine-grained control over individual node appearance compared to SVG-based alternatives, which may limit…", + "body": "A WebGL-based renderer gives less fine-grained control over individual node appearance compared to SVG-based alternatives, which may limit certain visual design options.", + "basis": "explicit", + "source": "derived-risk-or-question | stakeholder [RK2]", + "detail": null + }, + { + "local_id": 189, + "plane": "intent", + "kind": "context", + "title": "The smoke-webhook artifact has 4 frames: one initial frame (mode=initial, entryPhase=grounding, no parent) and three rederive frames (mode=…", + "body": "The smoke-webhook artifact has 4 frames: one initial frame (mode=initial, entryPhase=grounding, no parent) and three rederive frames (mode=rederive, entryPhase=shaping, all sharing the same triggerImpasseId, all parented to the initial frame). The three rederive frames form siblings at attemptNumber 0, 1, 2 — not a linear chain. The last rederive frame (attemptNumber=2) has nudgingActive=true. Snapshots reference all 4 frameIds in a single checkpoint at revision 4.", + "basis": "explicit", + "source": "technical-observed [X45]", + "detail": null + }, + { + "local_id": 190, + "plane": "intent", + "kind": "context", + "title": "Performance is managed through four mechanisms: (1) Web Worker layout: ForceAtlas2 runs off the main thread (covered in graph-layout-design…", + "body": "Performance is managed through four mechanisms: (1) Web Worker layout: ForceAtlas2 runs off the main thread (covered in graph-layout-design). (2) Sigma render batching: filter and selection state changes are debounced at 16ms before triggering a Sigma refresh, preventing per-keystroke re-renders during search. (3) Candidate/archived node toggling: when lifecycle visibility toggles change, node visibility is updated via Sigma's node attribute API (setting hidden=true/false) rather than rebuilding the graphology graph, which is O(nodes) not O(edges). (4) Provenance mini-graph depth cap: upstream traversal is capped at 50 nodes / depth-4 (covered in provenance-mini-graph-design). These four mechanisms together bound worst-case interaction latency for the reference dataset (376 active + 288 candidate + 88 archived nodes, 2662 edges).", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D20]", + "detail": null + }, + { + "local_id": 191, + "plane": "intent", + "kind": "context", + "title": "The explorer UI will live as a sibling package at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation-ui/.", + "body": "The explorer UI will live as a sibling package at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation-ui/.", + "basis": "explicit", + "source": "stakeholder [X13]", + "detail": null + }, + { + "local_id": 192, + "plane": "intent", + "kind": "criterion", + "title": "A browser network log captured during a full interaction session (artifact load, graph exploration, filtering, detail panel, comparison vie…", + "body": "A browser network log captured during a full interaction session (artifact load, graph exploration, filtering, detail panel, comparison view) must show zero requests to any API endpoint or server beyond the optional initial artifact.json fetch (when using the ?artifact= URL param). All data operations must be resolved from the in-memory indexes. Verified using browser DevTools Network tab or a network interception test.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR79]", + "detail": null + }, + { + "local_id": 193, + "plane": "oracle", + "kind": "evidence", + "title": "The smoke-webhook reference artifact contains 376 active nodes, 88 archived nodes, 288 candidate nodes, and 9 withdrawn nodes.", + "body": "The smoke-webhook reference artifact contains 376 active nodes, 88 archived nodes, 288 candidate nodes, and 9 withdrawn nodes.", + "basis": "explicit", + "source": "external-observed [E1]", + "detail": null + }, + { + "local_id": 194, + "plane": "intent", + "kind": "context", + "title": "The derivation story view must clearly show the regression/recovery narrative: impasse discovered → clean-room re-derivation → fan-out → pe…", + "body": "The derivation story view must clearly show the regression/recovery narrative: impasse discovered → clean-room re-derivation → fan-out → perspective selection → reconciliation.", + "basis": "explicit", + "source": "stakeholder [X36]", + "detail": null + }, + { + "local_id": 195, + "plane": "intent", + "kind": "context", + "title": "The spec-elicitation-ui project is in early design and planning.", + "body": "The spec-elicitation-ui project is in early design and planning. No implementation decisions beyond the tech stack (Vite, React, Tailwind) have been confirmed.", + "basis": "explicit", + "source": "stakeholder [X30]", + "detail": null + }, + { + "local_id": 196, + "plane": "intent", + "kind": "criterion", + "title": "When a search query is entered, matching nodes must be highlighted in the Sigma canvas (full intensity) simultaneously with a scrollable re…", + "body": "When a search query is entered, matching nodes must be highlighted in the Sigma canvas (full intensity) simultaneously with a scrollable results list appearing in the sidebar below the filter controls. Both the canvas highlight state and the results list must be visible at the same time without any tab switch or mode change. The results list must be sorted by displayId.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR37]", + "detail": null + }, + { + "local_id": 197, + "plane": "intent", + "kind": "requirement", + "title": "Each frame card in the macro timeline must display intervention annotation chips on its right edge, one chip per intervention record associ…", + "body": "Each frame card in the macro timeline must display intervention annotation chips on its right edge, one chip per intervention record associated with that frameId. Each chip must show the intervention kind and a count of targetNodeIds. Hovering a chip must show a tooltip listing the targetNodeIds as human-readable displayIds.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R46]", + "detail": null + }, + { + "local_id": 198, + "plane": "intent", + "kind": "requirement", + "title": "The macro timeline must lay out frames top-to-bottom chronologically on a main trunk.", + "body": "The macro timeline must lay out frames top-to-bottom chronologically on a main trunk. Rederive frames must branch horizontally to the right of their parent frame as sibling columns at the same vertical level. The reference artifact's structure (one initial frame with three sibling rederive frames, all sharing the same triggerImpasseId) must be correctly represented as horizontal siblings, not a linear chain.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R42]", + "detail": null + }, + { + "local_id": 199, + "plane": "intent", + "kind": "context", + "title": "Kael is an AI assistant with persistent memory, built as a CLI tool using TypeScript, Effect, and Deno.", + "body": "Kael is an AI assistant with persistent memory, built as a CLI tool using TypeScript, Effect, and Deno.", + "basis": "explicit", + "source": "external-observed [X1]", + "detail": null + }, + { + "local_id": 200, + "plane": "intent", + "kind": "criterion", + "title": "The full-text search input must match nodes whose text field contains the query string (case-insensitive) AND nodes whose displayId contain…", + "body": "The full-text search input must match nodes whose text field contains the query string (case-insensitive) AND nodes whose displayId contains the query string. A search for 'DEC' must return all decision hub nodes whose displayId begins with 'DEC'. A search for a term appearing only in node text (e.g. 'circuit breaker') must return those nodes. A search for a string present in neither field must return an empty results list with an appropriate empty-state message.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR83]", + "detail": null + }, + { + "local_id": 201, + "plane": "intent", + "kind": "context", + "title": "Validation report data (from reports/validation in artifact.json) is integrated as follows: (1) At load time a validationIssuesByEdgeId Map…", + "body": "Validation report data (from reports/validation in artifact.json) is integrated as follows: (1) At load time a validationIssuesByEdgeId Map is built from validation.json errors. Since errors are edge-centric (per validation-report-context), a secondary edgeIssuesByNodeId Map is derived by walking each errored edge's source and target nodeIds. (2) In the micro-view graph, nodes with validation issues are rendered with a red-tinted glow halo in addition to their normal phase-color glow, implemented as a second glow pass in the WebGL shader. (3) In the node detail panel, the Validation section lists all errors touching edges incident to this node, showing rule, severity, message, and the edge's type and direction. (4) A global validation summary badge in the toolbar shows total error count and pulses amber when errors exist.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D19]", + "detail": null + }, + { + "local_id": 202, + "plane": "intent", + "kind": "requirement", + "title": "Keyboard navigation must be restricted to HTML panel controls only.", + "body": "Keyboard navigation must be restricted to HTML panel controls only. The Sigma WebGL canvas must have no keyboard event handlers. The implemented keyboard bindings must include: Escape closes the detail panel and clears selection (or closes comparison overlay); Tab/Shift-Tab moves focus between toolbar controls, filter chips, and results list; Enter on a focused results-list row selects that node; Arrow keys navigate between results-list items when the list has focus.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R57]", + "detail": null + }, + { + "local_id": 203, + "plane": "intent", + "kind": "requirement", + "title": "The application must be deployable as a static site with no server-side runtime.", + "body": "The application must be deployable as a static site with no server-side runtime. All artifact data must be derived from the client-loaded artifact.json; no API calls to a backend are permitted.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R6]", + "detail": null + }, + { + "local_id": 204, + "plane": "oracle", + "kind": "evidence", + "title": "The smoke-webhook reference artifact contains 2,662 edges across 17 distinct edge types.", + "body": "The smoke-webhook reference artifact contains 2,662 edges across 17 distinct edge types.", + "basis": "explicit", + "source": "external-observed [E2]", + "detail": null + }, + { + "local_id": 205, + "plane": "intent", + "kind": "requirement", + "title": "The left sidebar filter panel must contain the following controls: (1) a full-text search input matching against node text and displayId; (…", + "body": "The left sidebar filter panel must contain the following controls: (1) a full-text search input matching against node text and displayId; (2) phase filter chips for all four phases (grounding, shaping, pinning, defining_done); (3) semantic role multi-select checkboxes for all ten roles (goal, term, context, constraint, evidence, design, alternative, requirement, criterion, risk); (4) hub type toggle (all / decision / justification / impasse / perspective); (5) epistemic status chips for all four values (observed, asserted, assumed, inferred); (6) authority chips for all four values (stakeholder, technical, external, derived); (7) lifecycle visibility toggles mirroring the toolbar toggles.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R25]", + "detail": null + }, + { + "local_id": 206, + "plane": "intent", + "kind": "context", + "title": "Because FrameRecord does not currently include a summary field (RK5, E5), the macro view frame cards gracefully degrade: if a frame has no…", + "body": "Because FrameRecord does not currently include a summary field (RK5, E5), the macro view frame cards gracefully degrade: if a frame has no summary, the summary region displays a muted placeholder reading 'NO SUMMARY AVAILABLE' in a dimmed monospace style consistent with the CRT aesthetic. The UI treats the summary field as optional throughout — no runtime error, no broken layout. When the pipeline schema extension is implemented and summaries are present in artifact.json, the UI renders them without any code change.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D16]", + "detail": null + }, + { + "local_id": 207, + "plane": "intent", + "kind": "criterion", + "title": "Pressing Tab repeatedly from the toolbar must cycle focus through all interactive controls in order: toolbar controls, filter chips in the…", + "body": "Pressing Tab repeatedly from the toolbar must cycle focus through all interactive controls in order: toolbar controls, filter chips in the sidebar, and results list rows. Pressing Shift-Tab must reverse the direction. Focus must never become trapped or jump to the Sigma WebGL canvas. Verified by simulating Tab keystrokes in a jsdom or browser test environment and asserting focused element identity at each step.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR72]", + "detail": null + }, + { + "local_id": 208, + "plane": "intent", + "kind": "criterion", + "title": "The Tailwind configuration must define all five CRT theme tokens with exact hex values: phosphor-amber (#FFB000), phosphor-green (#39FF14),…", + "body": "The Tailwind configuration must define all five CRT theme tokens with exact hex values: phosphor-amber (#FFB000), phosphor-green (#39FF14), phosphor-cyan (#00FFEF), phosphor-dim (#1A1A0F), and phosphor-text (#FFD580). Verified by reading tailwind.config.* and asserting each token name and value is present. At runtime, inspecting the computed background-color of the landing page body must return a value matching #1A1A0F (phosphor-dim).", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR68]", + "detail": null + }, + { + "local_id": 209, + "plane": "intent", + "kind": "constraint", + "title": "The app is strictly read-only.", + "body": "The app is strictly read-only. Editing nodes or edges is explicitly out of scope.", + "basis": "explicit", + "source": "stakeholder [C1]", + "detail": null + }, + { + "local_id": 210, + "plane": "intent", + "kind": "requirement", + "title": "The Connections section of the detail panel must include a collapsible 'Interventions' sub-section listing all intervention records that re…", + "body": "The Connections section of the detail panel must include a collapsible 'Interventions' sub-section listing all intervention records that reference the current node in their targetNodeIds array. Each entry must show the intervention kind, frameId (linked to the corresponding frame in the macro view), and createdAt timestamp. The interventionsByNodeId join must be pre-computed at load time.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R47]", + "detail": null + }, + { + "local_id": 211, + "plane": "intent", + "kind": "criterion", + "title": "Running the Deno bundler script (scripts/bundle-artifact.ts) against the smoke-webhook reference artifact directory must produce a single a…", + "body": "Running the Deno bundler script (scripts/bundle-artifact.ts) against the smoke-webhook reference artifact directory must produce a single artifact.json file whose top-level structure contains exactly the keys: manifest, sources, extractedClaims, interventions, graph (with sub-keys nodes, edges, frames, derivationRuns, fanInRecords, snapshots), and reports (with sub-key validation). The resulting file must be valid JSON parseable by JSON.parse() without error.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR75]", + "detail": null + }, + { + "local_id": 212, + "plane": "intent", + "kind": "context", + "title": "Justification hub nodes (hubType='justification') render in the detail panel's Connections section as: (1) a 'PREMISES' group showing nodes…", + "body": "Justification hub nodes (hubType='justification') render in the detail panel's Connections section as: (1) a 'PREMISES' group showing nodes connected by 'informed_by' edges (the upstream support nodes); (2) a 'CONCLUSIONS' group showing nodes connected by 'produced' edges (what this justification produced). The justification's text (its rationale statement) is shown in the Identity section as the primary text. This mirrors the ATMS-style justification model from the pipeline and enables users to trace exactly what combination of premises produced a given conclusion.", + "basis": "accepted_review_set", + "source": "derived-design-statement | derived-inferred [D26]", + "detail": null + }, + { + "local_id": 213, + "plane": "intent", + "kind": "criterion", + "title": "When the application is loaded with a ?artifact= query parameter, it must fetch the artifact.json from that URL via fetch(), skip the…", + "body": "When the application is loaded with a ?artifact= query parameter, it must fetch the artifact.json from that URL via fetch(), skip the drop zone entirely, and transition directly to the main explorer view. No file selection is required from the user.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR3]", + "detail": null + }, + { + "local_id": 214, + "plane": "intent", + "kind": "criterion", + "title": "Every interactive HTML element must display a visible focus ring styled in phosphor-amber (#FFB000) when it receives keyboard focus.", + "body": "Every interactive HTML element must display a visible focus ring styled in phosphor-amber (#FFB000) when it receives keyboard focus. Verified by: tabbing through all interactive elements in the toolbar, filter panel, and results list, and asserting that the focused element's outline or box-shadow computed value includes a color matching #FFB000. No interactive element may have an invisible or default-browser focus indicator.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR71]", + "detail": null + }, + { + "local_id": 215, + "plane": "intent", + "kind": "term", + "title": "Edge types are organized into six categories: hub-generic edges, decision hub e…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T10]", + "detail": { + "definition": "Edge types are organized into six categories: hub-generic edges, decision hub edges, perspective hub edges, impasse hub edges, content edges, and lineage edges." + } + }, + { + "local_id": 216, + "plane": "intent", + "kind": "term", + "title": "Review status is a tagged union on nodes with three variants: 'clean' (no issue…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T7]", + "detail": { + "definition": "Review status is a tagged union on nodes with three variants: 'clean' (no issues), 'suspect' (with causeIds indicating problems), and 'conditional' (with impasseIds indicating unresolved dependencies)." + } + }, + { + "local_id": 217, + "plane": "intent", + "kind": "constraint", + "title": "The subgraph zoom-into-frame feature for the macro view can be deferred to a later iteration.", + "body": "The subgraph zoom-into-frame feature for the macro view can be deferred to a later iteration.", + "basis": "explicit", + "source": "stakeholder [C10]", + "detail": null + }, + { + "local_id": 218, + "plane": "intent", + "kind": "criterion", + "title": "The micro-view toolbar must contain a range slider whose min and max correspond to the lowest and highest revision numbers present in the a…", + "body": "The micro-view toolbar must contain a range slider whose min and max correspond to the lowest and highest revision numbers present in the artifact's snapshots array. The slider must display a numeric revision badge and a human-readable timestamp label for the currently selected snapshot. A status line below the slider must show the revision number and the frameId(s) associated with that snapshot.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR29]", + "detail": null + }, + { + "local_id": 219, + "plane": "intent", + "kind": "requirement", + "title": "The Provenance section of the detail panel must render a second independent Sigma.js instance in approximately 280px of panel height, showi…", + "body": "The Provenance section of the detail panel must render a second independent Sigma.js instance in approximately 280px of panel height, showing the upstream derivation subgraph for the selected node. Traversal must follow support edges (derived_from, depends_on, informed_by) and hub-generic edges (produced, informed_by) backwards from the focal node. Traversal must be exhaustive for chains of 50 or fewer upstream nodes, and capped at depth 4 for larger chains. The focal node must appear at full glow. Ancestors must be laid out using graphology-layout-dagre in left-to-right derivation direction. Clicking any node in the mini-graph must navigate the main detail panel to that node.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R37]", + "detail": null + }, + { + "local_id": 220, + "plane": "intent", + "kind": "criterion", + "title": "The landing page drop zone must be visually styled with a phosphor-glowing dashed border (using the phosphor-amber or phosphor-green color…", + "body": "The landing page drop zone must be visually styled with a phosphor-glowing dashed border (using the phosphor-amber or phosphor-green color token), a scanline texture, and dark background consistent with the CRT aesthetic. No element on the landing page may render with default browser styling, white background, or unstyled text. The drop zone must provide a visible affordance (e.g., icon and label) indicating file drop or selection.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR94]", + "detail": null + }, + { + "local_id": 221, + "plane": "intent", + "kind": "criterion", + "title": "The provenance mini-graph must use the same Sigma WebGL node program class as the main micro-view graph.", + "body": "The provenance mini-graph must use the same Sigma WebGL node program class as the main micro-view graph. Node colors, glow style, and shape encoding (circle for content, diamond for hub) must be visually identical between the two Sigma instances. Verified by comparing the Sigma program constructor reference used in both instances — they must be the same class.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR51]", + "detail": null + }, + { + "local_id": 222, + "plane": "intent", + "kind": "criterion", + "title": "Pressing the Escape key while the detail panel is open (and no comparison overlay is open) must close the detail panel and set selectedNode…", + "body": "Pressing the Escape key while the detail panel is open (and no comparison overlay is open) must close the detail panel and set selectedNodeId to null in the Zustand store. The canvas must expand to fill the vacated space. Pressing Escape when both the comparison overlay and the detail panel are open must close only the comparison overlay and leave the detail panel visible.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR44]", + "detail": null + }, + { + "local_id": 223, + "plane": "intent", + "kind": "criterion", + "title": "When the upstream derivation chain of the selected node contains more than 50 nodes, the provenance mini-graph traversal must be capped at…", + "body": "When the upstream derivation chain of the selected node contains more than 50 nodes, the provenance mini-graph traversal must be capped at depth 4 from the focal node. When the chain is 50 nodes or fewer, traversal must be exhaustive. Verified by: selecting a deep-chain node from the reference artifact, confirming the mini-graph renders no more than depth-4 ancestors; then selecting a shallow-chain node and confirming all ancestors are rendered.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR50]", + "detail": null + }, + { + "local_id": 224, + "plane": "intent", + "kind": "context", + "title": "The CRT motif reinforces the idea of looking into a system's internals — it is the stakeholder's stated rationale for the visual design lan…", + "body": "The CRT motif reinforces the idea of looking into a system's internals — it is the stakeholder's stated rationale for the visual design language.", + "basis": "explicit", + "source": "stakeholder [X9]", + "detail": null + }, + { + "local_id": 225, + "plane": "intent", + "kind": "criterion", + "title": "Typing rapidly into the search input must not trigger a Sigma canvas refresh on every keystroke.", + "body": "Typing rapidly into the search input must not trigger a Sigma canvas refresh on every keystroke. Measuring Sigma refresh calls during a burst of 10 keystrokes within 100ms must show no more than one refresh call, occurring no sooner than 16ms after the last keystroke. Verified by spying on the Sigma refresh method in a test environment.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR39]", + "detail": null + }, + { + "local_id": 226, + "plane": "intent", + "kind": "criterion", + "title": "The toolbar must display a validation summary badge showing the total error count from validation.json.", + "body": "The toolbar must display a validation summary badge showing the total error count from validation.json. For the reference artifact, this count must match the number of entries in the errors array in validation.json. When errors are present, the badge must have a pulsing amber CSS animation. The badge must be present from the moment the main explorer renders, before any node is selected.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR54]", + "detail": null + }, + { + "local_id": 227, + "plane": "intent", + "kind": "context", + "title": "The spec elicitation source code lives at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation/.", + "body": "The spec elicitation source code lives at /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation/.", + "basis": "explicit", + "source": "external-observed [X6]", + "detail": null + }, + { + "local_id": 228, + "plane": "intent", + "kind": "term", + "title": "Edges in the graph are organized into categories: support edges (derived_from,…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T9]", + "detail": { + "definition": "Edges in the graph are organized into categories: support edges (derived_from, depends_on, informed_by) carry epistemic weight; workflow edges (produced, resolved_by, selected) carry operational provenance; structural edges (alternative_to, conflicts_with) are informational with no derivation direction." + } + }, + { + "local_id": 229, + "plane": "intent", + "kind": "term", + "title": "A semantic role classifies the epistemic function of a content node.", + "body": null, + "basis": "explicit", + "source": "technical-observed [T2]", + "detail": { + "definition": "A semantic role classifies the epistemic function of a content node. The ten defined values are: goal, term, context, constraint, evidence, design, alternative, requirement, criterion, and risk." + } + }, + { + "local_id": 230, + "plane": "intent", + "kind": "requirement", + "title": "In the micro-view graph, lifecycle state must be encoded as node opacity: active nodes at full opacity; candidate nodes at approximately 60…", + "body": "In the micro-view graph, lifecycle state must be encoded as node opacity: active nodes at full opacity; candidate nodes at approximately 60% opacity; archived nodes at approximately 20% opacity; withdrawn nodes at approximately 10% opacity.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R14]", + "detail": null + }, + { + "local_id": 231, + "plane": "intent", + "kind": "criterion", + "title": "At every point in the application lifecycle — loading, error, and empty (no artifact loaded) — the UI must display a bespoke CRT-themed tre…", + "body": "At every point in the application lifecycle — loading, error, and empty (no artifact loaded) — the UI must display a bespoke CRT-themed treatment. Inspecting the DOM during each state must show no element with default browser font (sans-serif or serif), no unstyled text, and no blank white areas.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR6]", + "detail": null + }, + { + "local_id": 232, + "plane": "intent", + "kind": "constraint", + "title": "The CRT visual motif must feel like a beautiful, refined instrument — not a retro novelty.", + "body": "The CRT visual motif must feel like a beautiful, refined instrument — not a retro novelty. The UI must have no janky transitions or raw unstyled states anywhere.", + "basis": "explicit", + "source": "stakeholder [C7]", + "detail": null + }, + { + "local_id": 233, + "plane": "intent", + "kind": "requirement", + "title": "The ForceAtlas2 layout computation for the micro-view graph must run in a Web Worker so the UI thread is not blocked.", + "body": "The ForceAtlas2 layout computation for the micro-view graph must run in a Web Worker so the UI thread is not blocked. During layout computation, the canvas must display a CRT-styled 'COMPUTING LAYOUT...' progress indicator. Layout positions must be cached in sessionStorage keyed by specId and snapshotRevision after the first computation.", + "basis": "accepted_review_set", + "source": "technical-inferred [R19]", + "detail": null + }, + { + "local_id": 234, + "plane": "intent", + "kind": "context", + "title": "The node detail panel shows kind-specific fields in its first collapsible section: for content nodes this is semantic role, epistemic statu…", + "body": "The node detail panel shows kind-specific fields in its first collapsible section: for content nodes this is semantic role, epistemic status, and authority; for hub nodes this is hub type.", + "basis": "explicit", + "source": "stakeholder [X39]", + "detail": null + }, + { + "local_id": 235, + "plane": "intent", + "kind": "context", + "title": "The stakeholder has defined two fundamental visualization views: a micro view and a macro view, both of which are required.", + "body": "The stakeholder has defined two fundamental visualization views: a micro view and a macro view, both of which are required.", + "basis": "explicit", + "source": "stakeholder [X19]", + "detail": null + }, + { + "local_id": 236, + "plane": "intent", + "kind": "criterion", + "title": "When archived nodes are made visible via the lifecycle toggle, they must render at approximately 20% opacity, visually distinct from active…", + "body": "When archived nodes are made visible via the lifecycle toggle, they must render at approximately 20% opacity, visually distinct from active nodes (100% opacity) and candidate nodes (~60% opacity). The dimmed appearance must be consistent with the CRT aesthetic (no bright white glow on archived nodes). Verified by enabling the archived toggle and visually comparing an archived node (e.g., D22 / id 00cfa668) against an active neighbor.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR78]", + "detail": null + }, + { + "local_id": 237, + "plane": "intent", + "kind": "context", + "title": "The validation.json report has a flat structure: timestamp, totalNodes, totalEdges, totalFrames, and an errors array where each error has r…", + "body": "The validation.json report has a flat structure: timestamp, totalNodes, totalEdges, totalFrames, and an errors array where each error has rule, severity, message, and edgeId. The predominant error rule observed is 'phase-stratification' flagging derived_from edges that cross phase boundaries (e.g. shaping→grounding, pinning→grounding). The report is edge-centric, not node-centric — issues reference edgeIds, not nodeIds directly.", + "basis": "explicit", + "source": "technical-observed [X46]", + "detail": null + }, + { + "local_id": 238, + "plane": "intent", + "kind": "requirement", + "title": "A Deno bundler script (scripts/bundle-artifact.ts) must merge all pipeline output files into artifact.json with the schema: { manifest, sou…", + "body": "A Deno bundler script (scripts/bundle-artifact.ts) must merge all pipeline output files into artifact.json with the schema: { manifest, sources, extractedClaims, interventions, graph: { nodes, edges, frames, derivationRuns, fanInRecords, snapshots }, reports: { validation } }. For each FrameRecord, the bundler must add summary: null when no summary is present, so the UI always receives a well-typed FrameRecord.summary field of type string | null.", + "basis": "accepted_review_set", + "source": "technical-inferred [R59]", + "detail": null + }, + { + "local_id": 239, + "plane": "intent", + "kind": "context", + "title": "Kael maintains a memory graph with nodes connected by typed edges such as reinforces, derived_from, and tension_with, and has sleep phases…", + "body": "Kael maintains a memory graph with nodes connected by typed edges such as reinforces, derived_from, and tension_with, and has sleep phases (nap, dream) that consolidate and maintain it.", + "basis": "explicit", + "source": "external-observed [X2]", + "detail": null + }, + { + "local_id": 240, + "plane": "intent", + "kind": "context", + "title": "Archived nodes must be visually distinct from active nodes (e.g.", + "body": "Archived nodes must be visually distinct from active nodes (e.g. dimmed or reduced opacity) in a manner consistent with the CRT aesthetic.", + "basis": "explicit", + "source": "stakeholder [X33]", + "detail": null + }, + { + "local_id": 241, + "plane": "intent", + "kind": "requirement", + "title": "The Tailwind configuration must define the following CRT theme tokens: phosphor-amber (#FFB000), phosphor-green (#39FF14), phosphor-cyan (#…", + "body": "The Tailwind configuration must define the following CRT theme tokens: phosphor-amber (#FFB000), phosphor-green (#39FF14), phosphor-cyan (#00FFEF), phosphor-dim (#1A1A0F) for backgrounds, and phosphor-text (#FFD580) for body text. These tokens must be used consistently across all UI components.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R53]", + "detail": null + }, + { + "local_id": 242, + "plane": "intent", + "kind": "term", + "title": "A provenance chain is the full upstream derivation path for a node: what it was…", + "body": null, + "basis": "explicit", + "source": "stakeholder [T18]", + "detail": { + "definition": "A provenance chain is the full upstream derivation path for a node: what it was derived from, what informed it, and what source material (quotes, claims) it is grounded in." + } + }, + { + "local_id": 243, + "plane": "intent", + "kind": "goal", + "title": "The macro view must enable users to understand how the spec developed over time — not just how it looks at a single point — showing the nar…", + "body": "The macro view must enable users to understand how the spec developed over time — not just how it looks at a single point — showing the narrative from initial grounding through derivation loops and reconciliation.", + "basis": "explicit", + "source": "stakeholder [G3]", + "detail": null + }, + { + "local_id": 244, + "plane": "oracle", + "kind": "evidence", + "title": "FrameRecord does not currently include a summary field; a schema extension is needed to add per-frame LLM-generated summaries to the artifa…", + "body": "FrameRecord does not currently include a summary field; a schema extension is needed to add per-frame LLM-generated summaries to the artifact.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [E5]", + "detail": null + }, + { + "local_id": 245, + "plane": "intent", + "kind": "requirement", + "title": "The application must accept artifact.json via browser File API drag-and-drop or file picker on a full-screen landing page, without requirin…", + "body": "The application must accept artifact.json via browser File API drag-and-drop or file picker on a full-screen landing page, without requiring any server upload or URL configuration from the user.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R1]", + "detail": null + }, + { + "local_id": 246, + "plane": "intent", + "kind": "context", + "title": "Active nodes are shown by default in the UI.", + "body": "Active nodes are shown by default in the UI. The user can toggle archived, candidate, and withdrawn nodes to see the full history.", + "basis": "explicit", + "source": "stakeholder [X32]", + "detail": null + }, + { + "local_id": 247, + "plane": "intent", + "kind": "context", + "title": "The smoke-test artifact for the webhook delivery system spec is located at /Users/bmahmoud/Desktop/smoke-webhook/ and serves as the referen…", + "body": "The smoke-test artifact for the webhook delivery system spec is located at /Users/bmahmoud/Desktop/smoke-webhook/ and serves as the reference dataset for development.", + "basis": "explicit", + "source": "external-observed [X7]", + "detail": null + }, + { + "local_id": 248, + "plane": "intent", + "kind": "criterion", + "title": "On initial load, only active nodes must be visible in the Sigma canvas.", + "body": "On initial load, only active nodes must be visible in the Sigma canvas. The three lifecycle toggles (archived, candidate, withdrawn) must each independently control visibility of their respective node sets. Toggling 'candidate' on must make the 288 candidate nodes from the reference artifact visible at ~60% opacity. Toggling it off must hide them. Active nodes must remain visible regardless of any toggle state.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR77]", + "detail": null + }, + { + "local_id": 249, + "plane": "intent", + "kind": "term", + "title": "A frame is a unit of derivation history in the pipeline.", + "body": null, + "basis": "explicit", + "source": "technical-observed [T12]", + "detail": { + "definition": "A frame is a unit of derivation history in the pipeline. The macro view shows frames and how they relate over time. Frames may carry LLM-generated summaries describing what happened and what was important." + } + }, + { + "local_id": 250, + "plane": "intent", + "kind": "requirement", + "title": "In the micro-view graph, node color must encode derivation phase using four distinct phosphor hues — one for each of the four phases (groun…", + "body": "In the micro-view graph, node color must encode derivation phase using four distinct phosphor hues — one for each of the four phases (grounding, shaping, pinning, defining_done). The same four-hue palette must be used consistently across the micro graph, the provenance mini-graph, and all phase badge UI elements.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R11]", + "detail": null + }, + { + "local_id": 251, + "plane": "intent", + "kind": "context", + "title": "For a decision node, the detail view must show its rationale, considered alternatives (via 'considered' edges), selection/rejection outcome…", + "body": "For a decision node, the detail view must show its rationale, considered alternatives (via 'considered' edges), selection/rejection outcomes (via 'selected'/'rejected' edges), and produced consequences (via 'consequence'/'produced' edges), with traceability back to grounding inputs.", + "basis": "explicit", + "source": "stakeholder [X35]", + "detail": null + }, + { + "local_id": 252, + "plane": "intent", + "kind": "criterion", + "title": "When the Provenance section is expanded for a selected node, a second independent Sigma.js instance must be mounted in a container of appro…", + "body": "When the Provenance section is expanded for a selected node, a second independent Sigma.js instance must be mounted in a container of approximately 280px height. The mini-graph must render the upstream derivation subgraph of the selected node, traversing support edges (derived_from, depends_on, informed_by) and hub-generic edges (produced, informed_by) backwards. The focal node must appear at full glow intensity. Ancestor layout must use graphology-layout-dagre in left-to-right direction.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR49]", + "detail": null + }, + { + "local_id": 253, + "plane": "intent", + "kind": "term", + "title": "A hub type identifies a node that aggregates structural reasoning rather than c…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T3]", + "detail": { + "definition": "A hub type identifies a node that aggregates structural reasoning rather than carrying content. The four defined hub types are: justification, decision, impasse, and perspective." + } + }, + { + "local_id": 254, + "plane": "intent", + "kind": "requirement", + "title": "At artifact load time, the application must build the following in-memory indexes in a single synchronous pass: nodeIndex (Map),…", + "body": "At artifact load time, the application must build the following in-memory indexes in a single synchronous pass: nodeIndex (Map), edgeIndex (Map), adjacency (Map), frameIndex (Map), snapshotIndex (Map), validationIssuesByEdgeId (Map), edgeIssuesByNodeId (Map), and interventionsByNodeId (Map). No re-parsing or re-indexing must occur during the session.", + "basis": "accepted_review_set", + "source": "technical-inferred [R18]", + "detail": null + }, + { + "local_id": 255, + "plane": "intent", + "kind": "context", + "title": "The file-loading mechanism must require zero configuration from the user.", + "body": "The file-loading mechanism must require zero configuration from the user.", + "basis": "explicit", + "source": "stakeholder [X42]", + "detail": null + }, + { + "local_id": 256, + "plane": "intent", + "kind": "requirement", + "title": "The Validation section of the detail panel must appear only when the node's review status is not 'clean'.", + "body": "The Validation section of the detail panel must appear only when the node's review status is not 'clean'. It must list all validation errors from validation.json that touch edges incident to the selected node, showing for each error: rule, severity, message, edge type, and edge direction. Suspect nodes must show causeId links and conditional nodes must show impasseId links.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [R39]", + "detail": null + }, + { + "local_id": 257, + "plane": "intent", + "kind": "criterion", + "title": "The Zustand store must expose the following top-level keys, all populated after artifact load: loadedArtifact, nodeIndex, edgeIndex, adjace…", + "body": "The Zustand store must expose the following top-level keys, all populated after artifact load: loadedArtifact, nodeIndex, edgeIndex, adjacency, frameIndex, snapshotIndex, validationIssuesByEdgeId, edgeIssuesByNodeId, interventionsByNodeId, activeView, selectedNodeId, selectedSnapshotRevision, filterState, comparisonState. Inspecting the store via a test or React DevTools must confirm all keys are present and correctly typed after a successful artifact parse.", + "basis": "accepted_review_set", + "source": "technical-inferred [CR32]", + "detail": null + }, + { + "local_id": 258, + "plane": "intent", + "kind": "criterion", + "title": "The top toolbar must contain a view-mode toggle control with exactly two states: Micro and Macro.", + "body": "The top toolbar must contain a view-mode toggle control with exactly two states: Micro and Macro. Activating Micro must mount the Sigma.js WebGL canvas in the central area. Activating Macro must unmount the Sigma canvas and mount the dedicated macro WebGL timeline canvas in its place. The toggle state must be reflected in the Zustand store's activeView field.", + "basis": "accepted_review_set", + "source": "stakeholder-inferred [CR12]", + "detail": null + }, + { + "local_id": 259, + "plane": "intent", + "kind": "term", + "title": "A SnapshotRecord is a checkpoint in the artifact that includes an activeNodeIds…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T11]", + "detail": { + "definition": "A SnapshotRecord is a checkpoint in the artifact that includes an activeNodeIds array indicating which nodes are active at that point in time, enabling the UI to reconstruct the graph state at any historical snapshot." + } + }, + { + "local_id": 260, + "plane": "intent", + "kind": "term", + "title": "A derivation phase is one of four ordered stages in the spec elicitation pipeli…", + "body": null, + "basis": "explicit", + "source": "technical-observed [T1]", + "detail": { + "definition": "A derivation phase is one of four ordered stages in the spec elicitation pipeline: grounding (goals, terms, constraints), shaping (designs, decisions, alternatives), pinning (requirements), and defining_done (acceptance criteria). Phases have strict dependency order." + } + }, + { + "local_id": 261, + "plane": "intent", + "kind": "constraint", + "title": "Polish in both design and interactions is essential: transitions must be smooth, hover states must feel alive, and the UI must reward explo…", + "body": "Polish in both design and interactions is essential: transitions must be smooth, hover states must feel alive, and the UI must reward exploration.", + "basis": "explicit", + "source": "stakeholder [C8]", + "detail": null + }, + { + "local_id": 262, + "plane": "intent", + "kind": "decision", + "title": "Use Sigma.js v3 with WebGL backend and a custom phosphor-glow fragment shader.", + "body": "Use Sigma.js v3 with WebGL backend and a custom phosphor-glow fragment shader.", + "basis": "explicit", + "source": "[DEC4]", + "detail": { + "chosen_option": "The micro-view graph is rendered using Sigma.js (v3) with a WebGL backend. Nodes are drawn with a custom WebGL fragment shader implementing per-node phosphor glow whose intensity is driven by a uniform updated on hover and selection state. Node color encodes derivation phase (4 distinct phosphor hues). Node shape encodes kind (circle = content, diamond = hub). Edge color encodes category: support edges in dim amber, workflow edges in brighter green, structural edges in muted cyan. Lifecycle state is encoded as opacity: active = full, archived = 20% opacity, candidate = 60% opacity, withdrawn = 10% opacity. The Sigma canvas is overlaid with a CSS scanline texture layer (pointer-events: none) to reinforce the CRT aesthetic.", + "rejected": [ + "Alternative: Use Cytoscape.js with its WebGL renderer (cytoscape-gl or pixi.js extension). Richer built-in layout algorithms and compound node support, but less control over custom shader effects and heavier bundle size.", + "Alternative: Render the graph using D3-force with SVG. Provides per-element CSS control, easy CRT filter effects via SVG filter primitives, and simpler hit-testing, but SVG degrades significantly beyond ~1,000 nodes and edges." + ], + "rationale": "Sigma.js is the stakeholder's stated preference (X16) and is purpose-built for large graph rendering via WebGL, directly addressing RK1 (376+ active nodes, 2,662 edges). Its custom WebGL program API allows implementing the phosphor glow shader per X40 with direct uniform control for hover intensity. D3/SVG (alt 1) cannot handle the dataset size at interactive frame rates. Cytoscape (alt 2) has heavier abstractions that would impede the custom shader work required for the CRT aesthetic, and RK2 notes that WebGL gives less fine-grained per-node control than SVG — Sigma's program API mitigates this by exposing shader-level control." + } + }, + { + "local_id": 263, + "plane": "intent", + "kind": "decision", + "title": "Right-side collapsible panel with CRT power-on flicker animation; four collapsible sections; embedded provenance mini-graph.", + "body": "Right-side collapsible panel with CRT power-on flicker animation; four collapsible sections; embedded provenance mini-graph.", + "basis": "explicit", + "source": "[DEC7]", + "detail": { + "chosen_option": "The right detail panel activates on node click with a ~150ms CRT power-on flicker animation (opacity pulses 0→0.3→0.1→1 over 150ms via CSS keyframes). The panel has four collapsible sections rendered top-to-bottom: (1) Identity — always expanded: full node text, displayId badge, phase badge, lifecycle badge, review status indicator (clean/suspect/conditional with cause links), kind-specific classification fields (semanticRole + epistemicStatus + authority for content nodes; hubType for hubs); (2) Connections — hub-type-specific relationship table: for decision hubs shows rationale prose, considered/selected/rejected/consequence edges grouped with linked displayIds per X35; for impasse hubs shows conflicting_input/resolved_by/spawned/refined_to; for justification hubs shows informed_by/produced; (3) Provenance — an embedded Sigma.js mini-graph (max ~50 upstream nodes) showing the full derivation chain per X25, with clickable nodes that navigate the main panel; (4) Validation — only shown when review status is not clean: lists suspect causeIds and conditional impasseIds with links, and lists any validation report errors touching this node's edges. Escape key closes the panel per X28.", + "rejected": [ + "Alternative: Open node detail as a full-screen modal overlay rather than a persistent side panel. Maximizes reading space but destroys the graph context while the detail is open, preventing navigation by clicking nodes in the background.", + "Alternative: Show node detail in a bottom drawer that expands upward, preserving the full left-right canvas width. Works well on wide monitors but reduces vertical canvas space significantly and is inconsistent with the three-region layout design." + ], + "rationale": "The right panel keeps the graph visible alongside the detail, enabling the user to follow provenance links in the mini-graph (X25) and click adjacent nodes without losing context — the modal (alt 1) destroys this. The bottom drawer (alt 2) cuts vertical canvas space, which is critical for the macro timeline view. The four-section collapsible structure satisfies X24's requirement that the most important information (Identity) is always visible at top. The flicker animation is explicitly preferred by the stakeholder (X24) over slide-in." + } + }, + { + "local_id": 264, + "plane": "intent", + "kind": "decision", + "title": "Keyboard navigation covers only HTML panel controls; Sigma canvas is mouse/touch only.", + "body": "Keyboard navigation covers only HTML panel controls; Sigma canvas is mouse/touch only.", + "basis": "explicit", + "source": "[DEC14]", + "detail": { + "chosen_option": "Keyboard navigation covers panel controls only, not the Sigma canvas (per X28 and C11). Implemented bindings: Escape closes the detail panel and clears node selection; Tab / Shift-Tab moves focus between toolbar controls, filter chips, and the results list; Enter on a focused results-list row selects that node (opens detail panel); Arrow keys navigate between results list items when the list has focus; Escape from the comparison overlay closes comparison and returns to the detail panel. All interactive HTML elements use standard focus rings styled in phosphor-amber to remain visible on the dark background. The Sigma canvas itself has no keyboard event handlers; it receives only mouse and touch events.", + "rejected": [ + "Alternative: Implement full ARIA graph navigation with keyboard traversal of graph nodes (focus moves between nodes via arrow keys, Tab enters/exits the graph). Significantly more accessible but explicitly out of scope per X28 and C11, and technically complex with a WebGL canvas." + ], + "rationale": "X28 and C11 explicitly restrict keyboard navigation to panel controls. Full ARIA graph traversal (alt 1) is explicitly out of scope and would require complex keyboard hit-testing against WebGL-rendered node positions. The defined bindings cover all panel interactions needed for productive exploration without a mouse." + } + }, + { + "local_id": 265, + "plane": "intent", + "kind": "decision", + "title": "Embedded second Sigma.js instance for provenance visualization, with dagre hierarchical layout and depth cap.", + "body": "Embedded second Sigma.js instance for provenance visualization, with dagre hierarchical layout and depth cap.", + "basis": "explicit", + "source": "[DEC8]", + "detail": { + "chosen_option": "The provenance mini-graph inside the detail panel is a second, independent Sigma.js instance mounted in a ~280px tall panel region. It renders only the upstream derivation subgraph for the selected node: traversing support edges (derived_from, depends_on, informed_by) and hub-generic edges (produced, informed_by) backwards from the focal node up to a configurable depth (default: exhaustive for graphs ≤50 upstream nodes, capped at depth-4 for larger chains). The focal node is rendered at full glow at center; ancestors are positioned using a left-to-right hierarchical layout (graphology-layout-dagre) to reflect derivation direction. Nodes are clickable: clicking navigates the main detail panel to that node, updating both the main graph selection and the mini-graph. Visual style (colors, glow, scanlines) is shared via the same Sigma program class used in the main graph.", + "rejected": [ + "Alternative: Replace the mini-graph with a structured text list of upstream nodes (grouped by edge type), each as a clickable pill. Avoids the complexity of a second Sigma instance (RK3) but loses the spatial/relational context that a graph provides." + ], + "rationale": "The stakeholder explicitly prefers a Sigma.js mini-graph for provenance (X25) and calls out that it must be visually coherent with the main graph. A text list (alt) satisfies navigation but not spatial provenance comprehension, which is central to G2 (tracing provenance). RK3 acknowledges the complexity; the depth cap (≤50 nodes / depth-4) bounds the worst-case rendering cost. Reusing the same Sigma program class minimizes the implementation delta and guarantees visual coherence." + } + }, + { + "local_id": 266, + "plane": "intent", + "kind": "decision", + "title": "Use a slider for snapshot selection, preserving graph topology by opacity rather than node removal.", + "body": "Use a slider for snapshot selection, preserving graph topology by opacity rather than node removal.", + "basis": "explicit", + "source": "[DEC9]", + "detail": { + "chosen_option": "The micro view is the default view on artifact load. It renders the full node+edge graph in Sigma.js with the snapshot selector in the top toolbar. The snapshot selector is a slider (with a numeric revision badge and timestamp label) that scrubs through SnapshotRecord revisions. On snapshot change, the active node set is recomputed from the selected snapshot's activeNodeIds array: nodes not in activeNodeIds are rendered at near-zero opacity (effectively hidden) rather than removed from the Sigma graph, preserving topology for context. A 'Show inactive' toggle in the toolbar reveals archived/candidate/withdrawn nodes at reduced opacity per X32 and X33. The current snapshot's revision number and frameId(s) are shown as a status line below the slider.", + "rejected": [ + "Alternative: Replace the snapshot slider with a dropdown menu listing each snapshot by revision number and timestamp. More explicit labeling but slower to scrub through revisions sequentially." + ], + "rationale": "X20 describes a 'dropdown or slider' but a slider affords scrubbing through the derivation history which is far more expressive for understanding temporal evolution (G3). Preserving topology (opacity vs removal) is essential so users retain spatial memory of node positions as they scrub — removing nodes would cause disorienting layout thrash since ForceAtlas2 positions are pinned after initial computation. The dropdown alt is retained as a labeled companion control (showing the current revision name) but the primary interaction is the slider." + } + }, + { + "local_id": 267, + "plane": "intent", + "kind": "decision", + "title": "Use Web Worker ForceAtlas2 layout computed at runtime, cached in sessionStorage.", + "body": "Use Web Worker ForceAtlas2 layout computed at runtime, cached in sessionStorage.", + "basis": "explicit", + "source": "[DEC5]", + "detail": { + "chosen_option": "The micro-view graph uses a force-directed layout (Sigma's built-in ForceAtlas2 via graphology-layout-forceatlas2) computed via a Web Worker on first load so the UI thread is not blocked. Layout positions are cached in sessionStorage keyed by specId+snapshotRevision. When the user scrubs to a different snapshot, only node visibility (opacity) changes — layout positions are not recomputed. The initial layout run is shown with a CRT-style 'COMPUTING LAYOUT...' progress indicator on the canvas.", + "rejected": [ + "Alternative: Use a hierarchical/DAG layout (e.g. graphology-layout-dagre) that reflects phase ordering (grounding → shaping → pinning → defining_done) top-to-bottom, trading force-directed organic clustering for explicit phase structure.", + "Alternative: Pre-compute and store layout positions in the artifact.json bundle at generation time, eliminating the Web Worker layout step entirely at the cost of larger artifact files." + ], + "rationale": "Force-directed layout naturally clusters semantically related nodes through the edge structure, which better serves G4's goal of understanding relationships than a rigid hierarchical layout. Pre-computing positions (alt 2) would bloat artifact.json and couple the bundler to layout logic that properly belongs in the UI. Hierarchical layout (alt 1) would produce a very tall graph given 376+ nodes across 4 phases and would degrade for the many cross-phase derived_from edges present in the reference dataset (per validation-report-context). Web Worker prevents UI jank during the ~1-2 second computation for the reference dataset size." + } + }, + { + "local_id": 268, + "plane": "intent", + "kind": "decision", + "title": "Tailwind theme tokens + CSS primitives for UI chrome; WebGL shader only for Sigma node glow.", + "body": "Tailwind theme tokens + CSS primitives for UI chrome; WebGL shader only for Sigma node glow. CSS blur filter used to approximate glow on HTML elements.", + "basis": "explicit", + "source": "[DEC12]", + "detail": { + "chosen_option": "The CRT visual design system is implemented as a Tailwind CSS theme extension plus a small set of reusable CSS/WebGL primitives. Tailwind theme tokens: phosphor-amber (#FFB000), phosphor-green (#39FF14), phosphor-cyan (#00FFEF), phosphor-dim (#1A1A0F) for backgrounds, phosphor-text (#FFD580) for body text. Typography: a monospaced font (JetBrains Mono or similar) for node text, displayIds, and data values; a slightly wider monospace for headers. CRT primitives: (a) scanline-overlay — a fixed CSS pseudo-element using a repeating-linear-gradient of 1px transparent / 1px rgba(0,0,0,0.15) stripes, pointer-events:none, placed above the WebGL canvas; (b) glow-text — a Tailwind utility applying text-shadow in the node's phase color; (c) flicker-in — a CSS @keyframes animation (0% opacity:0, 30% opacity:0.4, 45% opacity:0.1, 100% opacity:1) running 150ms ease-in used for panel power-on; (d) phosphor-border — a box-shadow utility combining inset and outer glow in the phase color at low alpha. All interactive elements (buttons, chips, panel headers) use hover states that intensify glow via CSS transition on box-shadow and text-shadow. No raw unstyled states exist: the loading state, error state, and empty states each have bespoke CRT-themed treatments.", + "rejected": [ + "Alternative: Implement all CRT effects purely in CSS (SVG filter feGaussianBlur for glow, CSS animations for flicker) without any WebGL shader involvement for the UI chrome, relying on Sigma's custom program only for node glow. Simpler but the glow effect on CSS elements will not match the WebGL node glow, creating visual inconsistency." + ], + "rationale": "Full CSS implementation (alt 1) was actually selected with a clarification: the node glow in Sigma is WebGL (per X40 and the graph-renderer decision), but all HTML UI elements use CSS box-shadow/text-shadow for glow effects — this is intentional. The visual gap between CSS glow (on panels, chips, buttons) and WebGL glow (on graph nodes) is acceptable and is bridged by matching the glow color palette. Attempting to route HTML element rendering through WebGL would be vastly over-engineered. The design system's value is in the Tailwind token vocabulary, the scanline overlay primitive, and the flicker-in keyframes, which together ensure no raw unstyled states exist (C7) and all transitions feel alive (C8)." + } + }, + { + "local_id": 269, + "plane": "intent", + "kind": "decision", + "title": "Build the macro timeline as a dedicated WebGL canvas (raw WebGL with a thin abstraction), separate from the Sigma micro-view canvas.", + "body": "Build the macro timeline as a dedicated WebGL canvas (raw WebGL with a thin abstraction), separate from the Sigma micro-view canvas.", + "basis": "explicit", + "source": "[DEC10]", + "detail": { + "chosen_option": "The macro view replaces the Sigma canvas with a WebGL-rendered vertical timeline built using raw WebGL (via a thin abstraction layer, not a graph library). The timeline lays out frames top-to-bottom chronologically on the main trunk. Rederive frames branch horizontally to the right of their parent frame as sibling columns at the same vertical level, reflecting the fan-out topology observed in the reference artifact (all three rederive attempts are siblings of the initial frame, not a linear chain). Each frame is rendered as a rectangular card with: frame mode badge (initial / rederive), entryPhase label, attemptNumber, nudgingActive indicator, createdAt timestamp, and the pre-generated LLM summary text if present (gracefully omitted with a 'no summary' placeholder if absent per RK5). Edges between frames encode relationship type: trunk-to-branch edges for triggerImpasseId linkage (drawn in warning amber), fan-in-record edges connecting rederive frames back to baseline (drawn in success green). Interventions associated with a frame are shown as small annotation chips on the frame card's right edge per X26. Clicking a frame card zooms the view to show which nodes changed in that frame (deferred per C10 to a later iteration — click opens a modal node-diff list instead).", + "rejected": [ + "Alternative: Represent frames as super-nodes in the same Sigma.js instance as the micro graph, using Sigma's camera zoom to transition between macro and micro views. Avoids a separate WebGL context but conflates two very different data models in one renderer, making the frame-card UI elements (text, badges, annotation chips) very difficult to implement.", + "Alternative: Build the macro timeline as a standard SVG/HTML component (e.g. using D3 for the layout math but rendering with React/SVG). Simpler to implement, easier to style with CSS, but does not enable the future zoom-into-frame WebGL transition that the stakeholder requires (X29)." + ], + "rationale": "X29 explicitly requires WebGL for the macro view to enable future zoom-into-frame. SVG/HTML (alt 1) cannot deliver a smooth zoom transition into the Sigma micro-graph. Reusing the Sigma instance (alt 2) conflates two incompatible data models and makes the rich frame-card UI (summaries, intervention chips, badges) nearly impossible within Sigma's node rendering model. A separate WebGL canvas gives full control over the frame-card visual language while keeping the door open for a seamless WebGL-to-WebGL zoom transition in a future iteration. The thin abstraction layer (rather than a full scene-graph library) keeps the bundle small and the rendering logic transparent." + } + }, + { + "local_id": 270, + "plane": "intent", + "kind": "decision", + "title": "Bundle all pipeline output into a single artifact.json; the UI loads only this file.", + "body": "Bundle all pipeline output into a single artifact.json; the UI loads only this file.", + "basis": "explicit", + "source": "[DEC1]", + "detail": { + "chosen_option": "A dedicated bundler script (part of the spec-elicitation package, not the UI) merges all pipeline output files into a single artifact.json. The merged structure is: { manifest, sources, extractedClaims, interventions, graph: { nodes, edges, frames, derivationRuns, fanInRecords, snapshots }, reports: { validation } }. The UI loads only this one file. The bundler is a Deno CLI script invoked after a pipeline run completes.", + "rejected": [ + "Alternative: Bundle the artifact as a ZIP archive containing the original directory structure; the UI uses a JS ZIP library to decompress and access files in-memory after the user drops the archive.", + "Alternative: The UI loads individual files lazily from a user-supplied directory path or URL prefix, fetching each file on demand rather than requiring a pre-bundled artifact.json." + ], + "rationale": "A single flat JSON file satisfies C6 (remote hosting compatibility) and X18 (File API drop zone) simultaneously: the user drops one file regardless of whether the app is local or remote-hosted. Lazy directory loading (alt 1) fails C6 when hosted remotely because browsers cannot access local filesystem paths. ZIP (alt 2) adds a decompression dependency and is less transparent/inspectable than plain JSON. The bundler lives in spec-elicitation (Deno/TypeScript), matching the existing toolchain. The merged schema is straightforward given the known file set (E4)." + } + }, + { + "local_id": 271, + "plane": "intent", + "kind": "decision", + "title": "Primary loading via browser File API drop zone; secondary loading via ?artifact= URL query param for remote sharing.", + "body": "Primary loading via browser File API drop zone; secondary loading via ?artifact= URL query param for remote sharing.", + "basis": "explicit", + "source": "[DEC2]", + "detail": { + "chosen_option": "The app opens on a full-screen landing page featuring a CRT-styled drop zone (phosphor-glowing dashed border, scanline texture). The user drops or selects artifact.json via the browser File API (no server upload). On successful parse the app transitions to the main explorer with a CRT power-on animation. An optional URL query param (?artifact=) allows linking to a remotely hosted artifact.json for sharing — the app fetches it via fetch() when present, bypassing the drop zone.", + "rejected": [ + "Alternative: Skip the File API entirely; require the user to host artifact.json at a URL and enter that URL in a text field. Simpler, but breaks the local-first zero-config requirement." + ], + "rationale": "File API drop zone satisfies X18 and X42 (zero config, local filesystem). The URL query param resolves RK6 (remote hosting compatibility) without complicating the primary path. URL-only (alt) violates X42. This dual-path design means both local and remote artifact access work against a static-hosted app, fully satisfying C6." + } + }, + { + "local_id": 272, + "plane": "intent", + "kind": "decision", + "title": "Show interventions in both the macro frame cards and the node detail panel, with a pre-computed interventionsByNodeId join index.", + "body": "Show interventions in both the macro frame cards and the node detail panel, with a pre-computed interventionsByNodeId join index.", + "basis": "explicit", + "source": "[DEC15]", + "detail": { + "chosen_option": "Interventions are displayed in two places per X26: (1) In the macro view, each frame card shows a row of small intervention chips on its right edge, one per intervention record associated with that frameId. Each chip shows the intervention kind (e.g. 'accept_candidate') and a count of targetNodeIds. Hovering a chip shows a tooltip listing the targetNodeIds as displayIds. (2) In the node detail panel, a collapsible 'Interventions' sub-section (within the Connections section) lists interventions that reference the current node in their targetNodeIds array, showing kind, frameId (linked to the macro view), and timestamp. The intervention-to-node join is pre-computed at load time as an interventionsByNodeId Map.", + "rejected": [ + "Alternative: Show interventions only in the macro view frame cards, not in the node detail panel. Lower implementation cost (avoids the interventionsByNodeId join), but loses the ability to see which interventions targeted a specific node from that node's perspective." + ], + "rationale": "X26 explicitly requires both locations. RK4 acknowledges the higher implementation cost but the stakeholder's preference is clear. The interventionsByNodeId Map is a simple O(n) pass over the interventions array at load time and adds negligible cost. Macro-only display (alt 1) would mean a user viewing a candidate node has no way to see that it was accepted by a human intervention without leaving the detail panel to find the frame — a significant navigation burden." + } + }, + { + "local_id": 273, + "plane": "intent", + "kind": "decision", + "title": "Split-panel overlay triggered from fan-in records or candidate node detail, showing text diff and fan-in rationale.", + "body": "Split-panel overlay triggered from fan-in records or candidate node detail, showing text diff and fan-in rationale.", + "basis": "explicit", + "source": "[DEC11]", + "detail": { + "chosen_option": "Side-by-side baseline/candidate comparison is triggered by: (1) clicking a fan-in record entry in the macro view, or (2) selecting a node with lifecycle=candidate and clicking a 'Compare' button in the detail panel. The comparison opens as a split overlay that temporarily replaces the right detail panel (or expands to full-panel width). The left column shows the baseline node (or the best_selected grouping winner from fan-in-records.json), the right column shows the candidate node. Differences in text, semantic role, epistemic status, and authority are highlighted using a line-diff style with phosphor-colored additions/deletions. The fan-in grouping rationale (from fan-in-records.json groupings[].rationale) is shown between the two columns as a decision banner. All nodes in the grouping are accessible via a tab row above the split. The comparison panel has a 'View in graph' action that focuses the main Sigma canvas on the baseline node.", + "rejected": [ + "Alternative: Show baseline and candidate as two differently-styled node clusters in the main Sigma graph simultaneously, with lineage edges (equivalent_to, refined_by etc.) highlighted between them. More spatially honest but visually overwhelming given the large candidate node count (288 candidates in the reference artifact)." + ], + "rationale": "The graph overlay alternative (alt 1) is impractical at the reference dataset scale: 288 candidate nodes rendered simultaneously with 376 active nodes would saturate the canvas and the AND-filter dimming model would conflict with comparison highlighting. The split-panel approach isolates the comparison to the specific grouping being examined (per fan-in-records groupings structure), which matches how reconciliation actually works in the pipeline. The fan-in rationale is the key semantic bridge between candidate and baseline and deserves a prominent display position, which the split panel's center banner provides." + } + }, + { + "local_id": 274, + "plane": "intent", + "kind": "decision", + "title": "Persistent left sidebar filter panel with inline results list.", + "body": "Persistent left sidebar filter panel with inline results list.", + "basis": "explicit", + "source": "[DEC6]", + "detail": { + "chosen_option": "The left sidebar hosts a filter+search panel with the following controls: (1) a full-text search input that matches against node text and displayId; (2) phase filter chips (grounding / shaping / pinning / defining_done); (3) semantic role multi-select checkboxes (10 roles from T2); (4) hub type toggle (all / decision / justification / impasse / perspective); (5) epistemic status chips; (6) authority chips; (7) lifecycle visibility toggles (active always on; archived/candidate/withdrawn toggleable per X32). All active filters combine with AND logic per X27. When any filter or search is active, Sigma re-renders with matching nodes at full glow intensity and non-matching nodes at 15% opacity; edges are dimmed when both endpoints are non-matching. The results panel below the filters shows a scrollable list of matching nodes sorted by displayId, each row showing displayId, phase badge, role/type badge, and truncated text.", + "rejected": [ + "Alternative: Replace the sidebar filter panel with a command-palette (Cmd+K style) overlay for search, with graph-level filter controls only on the toolbar. Saves sidebar space but separates search results from filter controls and reduces discoverability." + ], + "rationale": "X38 explicitly requires that search highlight nodes in the graph AND show a results list simultaneously — a command palette (alt 1) collapses after selection and cannot maintain a persistent results list alongside the live graph. The sidebar keeps all filter dimensions (phase, role, lifecycle, authority, epistemic status) visible and adjustable without modal interruption, which is essential for exploratory navigation of a 376+ node graph. AND-logic across all active filters (X27) is most natural to communicate in a persistent panel where users can see all active filter chips at once." + } + }, + { + "local_id": 275, + "plane": "intent", + "kind": "decision", + "title": "Use Zustand for application state management.", + "body": "Use Zustand for application state management.", + "basis": "explicit", + "source": "[DEC13]", + "detail": { + "chosen_option": "Application state is managed using Zustand (a lightweight React state manager). A single store holds: loadedArtifact (the parsed artifact.json), all derived indexes (nodeIndex, edgeIndex, adjacency, frameIndex, snapshotIndex, validationIndex), activeView ('micro' | 'macro'), selectedNodeId, selectedSnapshotRevision, filterState (lifecycle visibility, phase chips, role selection, search query), and comparisonState (active fan-in grouping). The store is initialized once on artifact load; all derived indexes are computed synchronously in a single pass and stored as plain Maps. React components subscribe to fine-grained store slices to minimize re-renders. No server state, no async store updates after load (C4).", + "rejected": [ + "Alternative: Use React Context + useReducer with no external state library. Zero dependencies, but Context re-renders on every state change unless carefully memoized — with a 376-node graph and frequent hover/filter state updates this would cause performance issues.", + "Alternative: Use Redux Toolkit for state management. More structured with time-travel debugging, but significantly more boilerplate for a read-only single-load application where immutability guarantees add no practical benefit." + ], + "rationale": "Zustand's slice-based subscription model is ideal for a read-only explorer: the graph canvas subscribes only to filter/selection state, the detail panel subscribes only to selectedNodeId, and the macro view subscribes only to activeView. This minimizes re-renders from hover and filter interactions on a 376+ node dataset. Redux (alt 1) is over-engineered for a read-only, single-load app with no async mutations. React Context (alt 2) would cause cascading re-renders on every filter keystroke unless heavily memoized, adding complexity that Zustand handles automatically." + } + }, + { + "local_id": 276, + "plane": "intent", + "kind": "decision", + "title": "Use a three-region resizable split layout: left sidebar (filter/search/results), central canvas, right detail panel.", + "body": "Use a three-region resizable split layout: left sidebar (filter/search/results), central canvas, right detail panel.", + "basis": "explicit", + "source": "[DEC3]", + "detail": { + "chosen_option": "The main explorer shell uses a three-region layout: (1) a narrow left sidebar containing the filter/search panel and a node-list results panel; (2) a large central canvas area that hosts either the micro-view graph or the macro-view timeline depending on the active view mode; (3) a right-side detail panel that slides/flickers into existence when a node is selected. A top toolbar holds the view-mode toggle (Micro / Macro), the snapshot selector (when in Micro mode), and global controls (lifecycle toggles, phase filter chips). All panels are resizable via drag handles. When no node is selected the right panel is collapsed and the canvas occupies the full remaining width.", + "rejected": [ + "Alternative: A fully tabbed layout where Micro View, Macro View, and Search are separate browser-tab-style panes with no persistent split panels, detail opens as a modal overlay.", + "Alternative: A fullscreen canvas-first layout with no persistent sidebar; filter/search and detail panel appear as HUD overlays on top of the canvas." + ], + "rationale": "The three-region layout keeps all primary navigation surfaces visible simultaneously, which is critical given G4's requirement for search + graph + detail in one view. The tabbed alternative (alt 1) fragments context — switching to search hides the graph, violating X38 (search must highlight in graph AND show results list simultaneously). Fullscreen HUD (alt 2) risks cluttering the canvas and makes the filter/results list difficult to use on smaller screens. Resizable panels give power users control over canvas real estate while keeping the layout coherent." + } + }, + { + "local_id": 277, + "plane": "intent", + "kind": "context", + "title": "The macro view must use a dedicated WebGL canvas to satisfy both the current frame-card UI requirements and the future zoom-into-frame tran…", + "body": "The macro view must use a dedicated WebGL canvas to satisfy both the current frame-card UI requirements and the future zoom-into-frame transition\n\n## Rationale\n\nX29 requires WebGL for the macro view to enable the future zoom-into-frame transition. D11 specifies rich frame-card content (badges, text, chips) that cannot be implemented within Sigma's node rendering model. DEC10 rejects SVG/HTML because it cannot deliver a smooth WebGL-to-WebGL zoom transition. These premises jointly require a dedicated raw WebGL canvas separate from Sigma.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J4]", + "detail": null + }, + { + "local_id": 278, + "plane": "intent", + "kind": "context", + "title": "Search must highlight matching nodes in the graph AND show a persistent results list simultaneously", + "body": "Search must highlight matching nodes in the graph AND show a persistent results list simultaneously\n\n## Rationale\n\nX38 requires both highlighting in the graph and a results list. DEC6 selects the persistent sidebar over a command palette precisely because a command palette cannot maintain a simultaneous results list. D7 specifies the Sigma opacity-based highlighting. These three premises jointly mandate that the results list is persistent and co-visible with the live graph, not a transient overlay.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J2]", + "detail": null + }, + { + "local_id": 279, + "plane": "intent", + "kind": "context", + "title": "Both File API drop zone and ?artifact= URL param are required to satisfy local-first and remote-hosting constraints simultaneously", + "body": "Both File API drop zone and ?artifact= URL param are required to satisfy local-first and remote-hosting constraints simultaneously\n\n## Rationale\n\nX18 and X42 require local filesystem loading with zero configuration. C6 requires the loading mechanism to work when hosted remotely. RK6 identifies these as potentially conflicting. DEC2 resolves the conflict by specifying dual-path loading. These four premises jointly mandate both loading mechanisms — neither alone is sufficient.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J3]", + "detail": null + }, + { + "local_id": 280, + "plane": "intent", + "kind": "context", + "title": "All micro-view design choices form a coherent, implementable system against the reference artifact.", + "body": "All micro-view design choices form a coherent, implementable system against the reference artifact.\n\n## Rationale\n\nThe graph-renderer-design (Sigma/WebGL), graph-data-model-design (in-memory indexes), graph-layout-design (Web Worker ForceAtlas2), filter-search-design (Zustand-driven opacity), micro-view-snapshot-design (opacity-based snapshot scrubbing), and performance-optimization-design (debouncing, hidden attribute) all interact without conflict: Sigma's node attribute API supports both opacity and hidden, ForceAtlas2 via graphology is the standard companion to Sigma, and Zustand's slice subscriptions prevent unnecessary Sigma refreshes. The reference dataset (761 total nodes, 2662 edges per validation-report-context) is within Sigma's documented performance envelope for WebGL rendering.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J1]", + "detail": null + } +] diff --git a/.fixtures/seed-specs/bilal-port/explorer-ui/spec.json b/.fixtures/seed-specs/bilal-port/explorer-ui/spec.json new file mode 100644 index 00000000..5e08428a --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/explorer-ui/spec.json @@ -0,0 +1,5 @@ +{ + "slug": "explorer-ui", + "name": "Explorer UI", + "readiness_grade": "commitments_ready" +} diff --git a/.fixtures/seed-specs/bilal-port/macro-view/edges.json b/.fixtures/seed-specs/bilal-port/macro-view/edges.json new file mode 100644 index 00000000..1bb837ea --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/macro-view/edges.json @@ -0,0 +1,4034 @@ +[ + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 103, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 122, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 142, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 1, + "target_local_id": 181, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 101, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 125, + "target_local_id": 230, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 197, + "target_local_id": 45, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 219, + "target_local_id": 96, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 40, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 110, + "target_local_id": 76, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 227, + "target_local_id": 25, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 73, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 204, + "target_local_id": 100, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 40, + "target_local_id": 157, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 55, + "target_local_id": 119, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 219, + "target_local_id": 57, + "stance": null, + "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": 228, + "target_local_id": 38, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 50, + "target_local_id": 120, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 100, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 98, + "target_local_id": 74, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 187, + "target_local_id": 38, + "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": 213, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 53, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 65, + "target_local_id": 210, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 230, + "target_local_id": 131, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 56, + "target_local_id": 207, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 43, + "target_local_id": 155, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 195, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 229, + "target_local_id": 168, + "stance": null, + "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": 221, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 202, + "target_local_id": 24, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 130, + "target_local_id": 196, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 128, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 43, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 68, + "target_local_id": 62, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 27, + "target_local_id": 147, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 13, + "target_local_id": 47, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 76, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 99, + "target_local_id": 230, + "stance": "for", + "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": 27, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 96, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 227, + "target_local_id": 41, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 80, + "target_local_id": 225, + "stance": null, + "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": 86, + "target_local_id": 182, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 73, + "target_local_id": 122, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 45, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 69, + "target_local_id": 227, + "stance": null, + "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": 75, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 44, + "target_local_id": 31, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 20, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 137, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 21, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 128, + "target_local_id": 151, + "stance": null, + "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": 86, + "target_local_id": 79, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 168, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 198, + "target_local_id": 226, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 92, + "target_local_id": 177, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 64, + "stance": null, + "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": 160, + "target_local_id": 41, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 88, + "target_local_id": 145, + "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": 68, + "target_local_id": 184, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 137, + "target_local_id": 148, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 229, + "target_local_id": 210, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 78, + "target_local_id": 208, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 159, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 231, + "target_local_id": 8, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 94, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 30, + "target_local_id": 77, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 211, + "target_local_id": 175, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 150, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 225, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 165, + "target_local_id": 23, + "stance": null, + "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": 150, + "target_local_id": 27, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 87, + "target_local_id": 68, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 174, + "target_local_id": 17, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 220, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 132, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 227, + "target_local_id": 192, + "stance": null, + "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, + "target_local_id": 4, + "stance": null, + "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": 218, + "target_local_id": 119, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 221, + "stance": null, + "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": 120, + "target_local_id": 124, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 105, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 112, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 184, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 155, + "target_local_id": 194, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 99, + "target_local_id": 215, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 145, + "target_local_id": 104, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 188, + "target_local_id": 74, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 115, + "target_local_id": 159, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 199, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 84, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 205, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 212, + "target_local_id": 48, + "stance": null, + "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, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 135, + "target_local_id": 88, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 219, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 177, + "target_local_id": 168, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 172, + "target_local_id": 215, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 87, + "target_local_id": 184, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 106, + "target_local_id": 161, + "stance": null, + "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, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 34, + "target_local_id": 42, + "stance": null, + "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": 221, + "target_local_id": 12, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 228, + "target_local_id": 116, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 6, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 12, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 37, + "target_local_id": 80, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 225, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 26, + "target_local_id": 38, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 73, + "target_local_id": 181, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 96, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 158, + "target_local_id": 119, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 10, + "target_local_id": 128, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 165, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 31, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 68, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 101, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 76, + "target_local_id": 12, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 6, + "target_local_id": 53, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 101, + "target_local_id": 138, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 205, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 81, + "target_local_id": 175, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 88, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 86, + "target_local_id": 157, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 39, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 69, + "target_local_id": 147, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 107, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 196, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 88, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 61, + "target_local_id": 231, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 231, + "target_local_id": 190, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 33, + "target_local_id": 230, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 113, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 201, + "target_local_id": 167, + "stance": null, + "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": 155, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 193, + "target_local_id": 195, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 113, + "target_local_id": 220, + "stance": null, + "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": 121, + "target_local_id": 25, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 149, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 16, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 44, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 95, + "target_local_id": 79, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 172, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 192, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 166, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 158, + "target_local_id": 55, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 126, + "target_local_id": 70, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 147, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 137, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 193, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 162, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 141, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 111, + "stance": null, + "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": 29, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 66, + "target_local_id": 113, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 121, + "target_local_id": 227, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 141, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 179, + "target_local_id": 41, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 161, + "target_local_id": 231, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 74, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 96, + "target_local_id": 232, + "stance": "for", + "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": 197, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 175, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 180, + "target_local_id": 231, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 70, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 102, + "target_local_id": 90, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 35, + "target_local_id": 30, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 196, + "target_local_id": 64, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 226, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 117, + "target_local_id": 121, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 198, + "target_local_id": 65, + "stance": null, + "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, + "target_local_id": 40, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 36, + "target_local_id": 129, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 52, + "target_local_id": 202, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 147, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 149, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 224, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 195, + "target_local_id": 138, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 30, + "target_local_id": 14, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 190, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 60, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 191, + "target_local_id": 134, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 35, + "target_local_id": 24, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 8, + "target_local_id": 180, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 85, + "target_local_id": 9, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 40, + "target_local_id": 104, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 55, + "target_local_id": 218, + "stance": null, + "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, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 224, + "target_local_id": 225, + "stance": null, + "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": 227, + "target_local_id": 147, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 211, + "target_local_id": 69, + "stance": null, + "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": 153, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 173, + "target_local_id": 71, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 202, + "target_local_id": 30, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 163, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 172, + "target_local_id": 169, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 138, + "stance": null, + "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, + "target_local_id": 51, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 15, + "target_local_id": 59, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 16, + "target_local_id": 86, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 162, + "target_local_id": 56, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 106, + "target_local_id": 124, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 157, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 169, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 225, + "target_local_id": 215, + "stance": null, + "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, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 48, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 34, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 135, + "target_local_id": 145, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 165, + "target_local_id": 207, + "stance": null, + "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, + "target_local_id": 46, + "stance": null, + "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, + "target_local_id": 229, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 29, + "target_local_id": 77, + "stance": null, + "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": 217, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 131, + "target_local_id": 99, + "stance": null, + "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, + "target_local_id": 207, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 76, + "target_local_id": 157, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 51, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 13, + "target_local_id": 147, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 134, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 56, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 160, + "target_local_id": 179, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 57, + "target_local_id": 105, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 59, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 68, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 231, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 85, + "target_local_id": 118, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 53, + "target_local_id": 228, + "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": 93, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 90, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 95, + "target_local_id": 182, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 58, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 52, + "target_local_id": 14, + "stance": null, + "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, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 167, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 109, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 69, + "target_local_id": 175, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 201, + "target_local_id": 220, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 70, + "target_local_id": 132, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 70, + "target_local_id": 148, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 184, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 228, + "target_local_id": 178, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 110, + "target_local_id": 157, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 141, + "target_local_id": 116, + "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": 219, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 81, + "target_local_id": 2, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 144, + "target_local_id": 106, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 170, + "target_local_id": 77, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 128, + "target_local_id": 194, + "stance": null, + "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, + "target_local_id": 138, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 5, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 98, + "target_local_id": 141, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 9, + "target_local_id": 118, + "stance": null, + "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, + "target_local_id": 178, + "stance": null, + "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": 196, + "target_local_id": 148, + "stance": null, + "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, + "target_local_id": 64, + "stance": null, + "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, + "target_local_id": 225, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 98, + "target_local_id": 116, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 177, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 27, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 186, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 21, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 143, + "target_local_id": 100, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 169, + "target_local_id": 215, + "stance": null, + "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, + "target_local_id": 124, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 228, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 92, + "target_local_id": 229, + "stance": null, + "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, + "target_local_id": 105, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 54, + "target_local_id": 8, + "stance": null, + "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": 223, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 175, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 185, + "target_local_id": 137, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 216, + "target_local_id": 61, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 152, + "target_local_id": 122, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 79, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 126, + "target_local_id": 132, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 47, + "target_local_id": 227, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 123, + "target_local_id": 22, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 217, + "target_local_id": 231, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 196, + "target_local_id": 223, + "stance": null, + "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, + "target_local_id": 26, + "stance": null, + "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, + "target_local_id": 213, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 156, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 139, + "target_local_id": 18, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 217, + "target_local_id": 124, + "stance": null, + "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, + "target_local_id": 23, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 127, + "target_local_id": 75, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 138, + "target_local_id": 232, + "stance": "for", + "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, + "target_local_id": 96, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 45, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 213, + "target_local_id": 21, + "stance": null, + "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, + "target_local_id": 119, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 99, + "target_local_id": 33, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 166, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 22, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 46, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 129, + "target_local_id": 205, + "stance": null, + "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": 52, + "target_local_id": 30, + "stance": null, + "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": 133, + "target_local_id": 131, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 86, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 132, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 212, + "target_local_id": 102, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 55, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 195, + "target_local_id": 226, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 54, + "target_local_id": 180, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 53, + "target_local_id": 7, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 193, + "target_local_id": 138, + "stance": null, + "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": 17, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 205, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 74, + "target_local_id": 228, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 228, + "target_local_id": 183, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 120, + "target_local_id": 229, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 91, + "target_local_id": 229, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 58, + "target_local_id": 226, + "stance": null, + "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, + "target_local_id": 93, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 31, + "target_local_id": 189, + "stance": null, + "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": 115, + "target_local_id": 231, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 95, + "target_local_id": 163, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 113, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 102, + "target_local_id": 48, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 129, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 152, + "target_local_id": 73, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 145, + "target_local_id": 40, + "stance": null, + "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, + "target_local_id": 119, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 71, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 202, + "target_local_id": 72, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 101, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 31, + "target_local_id": 12, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 204, + "target_local_id": 220, + "stance": null, + "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, + "target_local_id": 25, + "stance": null, + "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, + "target_local_id": 200, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 216, + "target_local_id": 149, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 80, + "target_local_id": 99, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 49, + "target_local_id": 101, + "stance": null, + "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, + "target_local_id": 215, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 23, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 196, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 58, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 74, + "target_local_id": 119, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 106, + "target_local_id": 217, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 77, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 140, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 190, + "target_local_id": 161, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 157, + "stance": null, + "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, + "target_local_id": 5, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 2, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 15, + "target_local_id": 219, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 11, + "target_local_id": 153, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 186, + "target_local_id": 58, + "stance": null, + "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, + "target_local_id": 184, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 219, + "target_local_id": 157, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 195, + "target_local_id": 189, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 215, + "target_local_id": 230, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 82, + "target_local_id": 223, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 50, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 174, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 18, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 55, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 84, + "target_local_id": 90, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 129, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 163, + "target_local_id": 19, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 77, + "stance": null, + "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": 217, + "target_local_id": 210, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 73, + "target_local_id": 220, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 59, + "target_local_id": 105, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 179, + "target_local_id": 209, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 129, + "target_local_id": 142, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 29, + "target_local_id": 67, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 35, + "target_local_id": 202, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 232, + "target_local_id": 18, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 166, + "target_local_id": 226, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "realization", + "source_local_id": 230, + "target_local_id": 80, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 75, + "target_local_id": 63, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 139, + "target_local_id": 20, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 2, + "target_local_id": 175, + "stance": null, + "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, + "target_local_id": 99, + "stance": null, + "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, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 125, + "target_local_id": 224, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 115, + "target_local_id": 61, + "stance": null, + "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, + "target_local_id": 21, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 32, + "target_local_id": 84, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 17, + "target_local_id": 89, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 163, + "target_local_id": 79, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 218, + "target_local_id": 148, + "stance": null, + "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, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 111, + "target_local_id": 214, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 159, + "target_local_id": 180, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 153, + "target_local_id": 28, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 108, + "target_local_id": 58, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 5, + "target_local_id": 192, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 154, + "target_local_id": 83, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 176, + "target_local_id": 227, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 42, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 60, + "stance": null, + "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, + "target_local_id": 176, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 229, + "target_local_id": 203, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 154, + "target_local_id": 60, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 220, + "target_local_id": 90, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 139, + "target_local_id": 232, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 221, + "target_local_id": 60, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 199, + "target_local_id": 217, + "stance": null, + "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, + "target_local_id": 218, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 167, + "target_local_id": 222, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 223, + "target_local_id": 148, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 133, + "target_local_id": 224, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 222, + "target_local_id": 22, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 189, + "target_local_id": 232, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 91, + "target_local_id": 97, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 221, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 83, + "target_local_id": 171, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 74, + "target_local_id": 178, + "stance": null, + "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, + "target_local_id": 181, + "stance": "for", + "basis": "explicit", + "rationale": null + }, + { + "category": "dependency", + "source_local_id": 170, + "target_local_id": 29, + "stance": null, + "basis": "explicit", + "rationale": null + }, + { + "category": "support", + "source_local_id": 134, + "target_local_id": 103, + "stance": "for", + "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, + "target_local_id": 166, + "stance": null, + "basis": "explicit", + "rationale": null + } +] diff --git a/.fixtures/seed-specs/bilal-port/macro-view/nodes.json b/.fixtures/seed-specs/bilal-port/macro-view/nodes.json new file mode 100644 index 00000000..60a2e7b6 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/macro-view/nodes.json @@ -0,0 +1,2436 @@ +[ + { + "local_id": 1, + "plane": "oracle", + "kind": "check", + "title": "Macro View — code-audit pass", + "body": "Synthetic parent check representing the manual code-audit pass during which evidence nodes were authored. Generated by .fixtures/seed-specs/bilal-port/_port-script.ts to give imported evidence a structural parent on the oracle plane.", + "basis": "explicit", + "source": "derived-port-synthetic", + "detail": null + }, + { + "local_id": 2, + "plane": "intent", + "kind": "requirement", + "title": "PerspectiveNodes whose perspectiveStatus is 'selected' shall render at full opacity; PerspectiveNodes whose perspectiveStatus is 'rejected'…", + "body": "PerspectiveNodes whose perspectiveStatus is 'selected' shall render at full opacity; PerspectiveNodes whose perspectiveStatus is 'rejected' (or 'open' on a non-taken branch) shall render at reduced opacity (approximately 30%) to indicate they were not taken.", + "basis": "explicit", + "source": "derived [R33]", + "detail": null + }, + { + "local_id": 3, + "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": 4, + "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": 5, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall reuse the existing src/components/DetailPanel.tsx component for the right-side detail panel; no separate macro-specifi…", + "body": "The macro view shall reuse the existing src/components/DetailPanel.tsx component for the right-side detail panel; no separate macro-specific detail panel component shall be introduced. DetailPanel may be extended internally to branch on the new record kinds (frame, run, fan-in, reconciliation, perspective).", + "basis": "explicit", + "source": "derived [R30]", + "detail": null + }, + { + "local_id": 6, + "plane": "intent", + "kind": "criterion", + "title": "Within a single frame containing all four phases, the y-coordinates of the four PhaseGroupNodes satisfy y(defining_done) < y(pinning) < y(s…", + "body": "Within a single frame containing all four phases, the y-coordinates of the four PhaseGroupNodes satisfy y(defining_done) < y(pinning) < y(shaping) < y(grounding) (defining_done at top of frame visually). Verify with a unit-test fixture.", + "basis": "explicit", + "source": "derived [CR20]", + "detail": null + }, + { + "local_id": 7, + "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": 8, + "plane": "intent", + "kind": "requirement", + "title": "Collapsed/expanded state shall not be written to localStorage, sessionStorage, cookies, the URL/query string, IndexedDB, or any other persi…", + "body": "Collapsed/expanded state shall not be written to localStorage, sessionStorage, cookies, the URL/query string, IndexedDB, or any other persistence layer; it shall exist only in React in-memory state for the lifetime of the component instance.", + "basis": "explicit", + "source": "derived [R24]", + "detail": null + }, + { + "local_id": 9, + "plane": "intent", + "kind": "requirement", + "title": "The macro view layout shall be implemented as a custom recursive (DFS) algorithm computing absolute positions and subtree bounding boxes; i…", + "body": "The macro view layout shall be implemented as a custom recursive (DFS) algorithm computing absolute positions and subtree bounding boxes; it shall not use dagre, ELK, or any other general-purpose graph layout library.", + "basis": "explicit", + "source": "derived [R21]", + "detail": null + }, + { + "local_id": 10, + "plane": "intent", + "kind": "criterion", + "title": "User can pan and zoom the canvas: simulating a mouse-wheel event over the React Flow pane changes the viewport zoom level, and a mouse-drag…", + "body": "User can pan and zoom the canvas: simulating a mouse-wheel event over the React Flow pane changes the viewport zoom level, and a mouse-drag on the pane changes the viewport translation. Verify with a React Flow integration test using fireEvent.wheel and fireEvent.mouseDown/Move/Up, asserting useReactFlow().getViewport() values change accordingly.", + "basis": "explicit", + "source": "derived [CR10]", + "detail": null + }, + { + "local_id": 11, + "plane": "intent", + "kind": "criterion", + "title": "For each ReconciliationRecord with non-empty resolvedImpasseIds, layout emits a resolution edge from the corresponding ReconciliationNode t…", + "body": "For each ReconciliationRecord with non-empty resolvedImpasseIds, layout emits a resolution edge from the corresponding ReconciliationNode to each resolved ImpasseNode. Edge has computed stroke color resolving to the resolving phase's --color-phase-* token, stroke-style solid, markerEnd arrow, and the routing path returns leftward (i.e., target node x < source node x, or via a custom edge component that produces a leftward bend). Verify with a fixture asserting source.x > target.x and computed style.", + "basis": "explicit", + "source": "derived [CR55]", + "detail": null + }, + { + "local_id": 12, + "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": 13, + "plane": "intent", + "kind": "criterion", + "title": "Clicking on a PhantomNode in the rendered macro view does NOT dispatch any select action and does not change global selection state.", + "body": "Clicking on a PhantomNode in the rendered macro view does NOT dispatch any select action and does not change global selection state. The PhantomNode's DOM element exposes no role='button' or interactive cursor style. Verify with an RTL test: render fixture with phantom, click it, assert store.select spy was not called and computed style cursor != 'pointer'.", + "basis": "explicit", + "source": "derived [CR32]", + "detail": null + }, + { + "local_id": 14, + "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": 15, + "plane": "intent", + "kind": "criterion", + "title": "A DerivationRunNode with status='running' has: (a) a CSS animation property whose name is or includes 'phosphor-arrive' on the node body; (…", + "body": "A DerivationRunNode with status='running' has: (a) a CSS animation property whose name is or includes 'phosphor-arrive' on the node body; (b) a header chip with textContent 'RUNNING' and computed color resolving to --color-phosphor-cyan; (c) a descendant element with a CSS animation that visibly translates across the node interior (scanline sweep). The node remains clickable: click dispatches a select action. Verify with an RTL test asserting style.animationName, chip text and color, and click dispatch.", + "basis": "explicit", + "source": "derived [CR45]", + "detail": null + }, + { + "local_id": 16, + "plane": "intent", + "kind": "criterion", + "title": "Static-analysis scan of src/components/macro/**/*.{ts,tsx} finds no imports from @mui/*, @chakra-ui/*, antd, react-bootstrap, or other gene…", + "body": "Static-analysis scan of src/components/macro/**/*.{ts,tsx} finds no imports from @mui/*, @chakra-ui/*, antd, react-bootstrap, or other generic UI component libraries. CSS scan finds no border-radius value greater than 4px on any macro view selector (allowing only sharp/squared corners). No element uses a Tailwind class indicating blue primary buttons (e.g., bg-blue-*) for primary actions. Verify with combined import-graph and CSS-grep tests.", + "basis": "explicit", + "source": "derived [CR37]", + "detail": null + }, + { + "local_id": 17, + "plane": "intent", + "kind": "requirement", + "title": "All colors, fonts, surfaces, glow, and scanline treatments used by macro view components shall be drawn from existing tokens defined in src…", + "body": "All colors, fonts, surfaces, glow, and scanline treatments used by macro view components shall be drawn from existing tokens defined in src/styles/theme.css (oklch phosphor palette, --font-mono, --color-surface-0..3, --color-phosphor-*, --color-phase-*, --color-text-*). Macro view code shall not introduce hard-coded hex/rgb/hsl colors or new top-level palette tokens.", + "basis": "explicit", + "source": "derived [R34]", + "detail": null + }, + { + "local_id": 18, + "plane": "intent", + "kind": "requirement", + "title": "No single visual channel on a macro node shall encode more than one semantic attribute.", + "body": "No single visual channel on a macro node shall encode more than one semantic attribute. Specifically: phase color shall encode only phase identity; border color shall encode only run/reconciliation outcome (red=failed/bail, amber=retry/nudging-related, cyan=recurse/running, phase color=accepted); border style shall encode only frame mode; fill/dim level shall encode only failure-or-bail status; opacity shall encode only perspective selectedness; shape (diamond) shall encode only impasse identity. New attributes shall not be added to existing channels without re-justifying all collisions.", + "basis": "explicit", + "source": "derived [R64]", + "detail": null + }, + { + "local_id": 19, + "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": 20, + "plane": "intent", + "kind": "constraint", + "title": "Color additions to the palette must be semantically justified, not decorative — every color must earn its place by carrying meaning a user…", + "body": "Color additions to the palette must be semantically justified, not decorative — every color must earn its place by carrying meaning a user needs to distinguish at a glance.", + "basis": "explicit", + "source": "external [C7]", + "detail": null + }, + { + "local_id": 21, + "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": 22, + "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": 23, + "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": 24, + "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": 25, + "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": 26, + "plane": "intent", + "kind": "requirement", + "title": "The layout shall size each depth lane's width as a function of the maximum content width across nodes at that depth, rather than using a si…", + "body": "The layout shall size each depth lane's width as a function of the maximum content width across nodes at that depth, rather than using a single fixed lane-width constant for all depths.", + "basis": "explicit", + "source": "derived [R19]", + "detail": null + }, + { + "local_id": 27, + "plane": "intent", + "kind": "requirement", + "title": "Each PhantomNode shall render as a dashed-outline ghost tile with no fill, bearing a label identifying it as a phantom (e.g., 'PHANTOM — no…", + "body": "Each PhantomNode shall render as a dashed-outline ghost tile with no fill, bearing a label identifying it as a phantom (e.g., 'PHANTOM — no perspective taken').", + "basis": "explicit", + "source": "derived [R50]", + "detail": null + }, + { + "local_id": 28, + "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": 29, + "plane": "intent", + "kind": "requirement", + "title": "A DerivationRunNode whose status is 'failed' shall render with a border in --color-phosphor-red and a visibly dimmed interior.", + "body": "A DerivationRunNode whose status is 'failed' shall render with a border in --color-phosphor-red and a visibly dimmed interior.", + "basis": "explicit", + "source": "derived [R43]", + "detail": null + }, + { + "local_id": 30, + "plane": "intent", + "kind": "context", + "title": "The bail reconciliation outcome and a failed run share the same red-border/dimmed-interior treatment by design, which could make the two vi…", + "body": "The bail reconciliation outcome and a failed run share the same red-border/dimmed-interior treatment by design, which could make the two visually indistinguishable at a glance.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK2]", + "detail": null + }, + { + "local_id": 31, + "plane": "intent", + "kind": "requirement", + "title": "Each PhaseGroupNode shall render with: a 1px border in its phase color (drawn from --color-phase-*), a warm dark fill from --color-surface-…", + "body": "Each PhaseGroupNode shall render with: a 1px border in its phase color (drawn from --color-phase-*), a warm dark fill from --color-surface-1, a scanline overlay, and a header line displaying the phase name, frame displayId, and frame mode in --color-text-secondary.", + "basis": "explicit", + "source": "derived [R38]", + "detail": null + }, + { + "local_id": 32, + "plane": "intent", + "kind": "criterion", + "title": "Importing MacroView from src/components/MacroView (the original path used by routes/explore.tsx) resolves to a working React component that…", + "body": "Importing MacroView from src/components/MacroView (the original path used by routes/explore.tsx) resolves to a working React component that renders without throwing. Verify with a Vitest + React Testing Library render test that imports from the legacy path and asserts the component mounts.", + "basis": "explicit", + "source": "derived [CR2]", + "detail": null + }, + { + "local_id": 33, + "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": 34, + "plane": "intent", + "kind": "criterion", + "title": "package.json declares @xyflow/react at major version 12 (e.g., ^12.x), and the rendered MacroView DOM contains the React Flow root element…", + "body": "package.json declares @xyflow/react at major version 12 (e.g., ^12.x), and the rendered MacroView DOM contains the React Flow root element wrapped by a ReactFlowProvider. Verify by (a) inspecting package.json with a unit test, and (b) a render test asserting that useReactFlow() called inside a child node throws no provider-missing error.", + "basis": "explicit", + "source": "derived [CR3]", + "detail": null + }, + { + "local_id": 35, + "plane": "intent", + "kind": "criterion", + "title": "An ImpasseNode whose hub.id appears in some FrameRecord.triggerImpasseIds where that frame's terminal ReconciliationRecord.outcome === 'bai…", + "body": "An ImpasseNode whose hub.id appears in some FrameRecord.triggerImpasseIds where that frame's terminal ReconciliationRecord.outcome === 'bail' renders with a chip element whose textContent matches /DEAD[\\s-]?END/i. An ImpasseNode whose triggered frame did not bail (or that has no triggered frame) does NOT render this chip. Verify with two RTL fixtures.", + "basis": "explicit", + "source": "derived [CR50]", + "detail": null + }, + { + "local_id": 36, + "plane": "intent", + "kind": "criterion", + "title": "A PhaseGroupNode for FrameRecord.mode='initial' has computed border-style 'solid'; mode='rederive' has 'double'; mode='grounding_enrichment…", + "body": "A PhaseGroupNode for FrameRecord.mode='initial' has computed border-style 'solid'; mode='rederive' has 'double'; mode='grounding_enrichment' has 'dashed'. The header additionally contains a text chip whose textContent equals the mode value (case-insensitive). Verify with parameterized RTL tests across all three modes.", + "basis": "explicit", + "source": "derived [CR40]", + "detail": null + }, + { + "local_id": 37, + "plane": "intent", + "kind": "criterion", + "title": "MacroView renders a button with accessible name matching /reload/i.", + "body": "MacroView renders a button with accessible name matching /reload/i. Clicking it re-invokes the artifact loader and produces a fresh React Flow node array (new array identity) reflecting any updated underlying data. Verify with React Testing Library: query button by role/name, click, assert loader spy called twice and that the rendered output reflects mutated mock data after the second load.", + "basis": "explicit", + "source": "derived [CR7]", + "detail": null + }, + { + "local_id": 38, + "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": 39, + "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": 40, + "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": 41, + "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": 42, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall render its graph using React Flow (@xyflow/react) at major version 12, mounted inside a ReactFlowProvider.", + "body": "The macro view shall render its graph using React Flow (@xyflow/react) at major version 12, mounted inside a ReactFlowProvider.", + "basis": "explicit", + "source": "derived [R3]", + "detail": null + }, + { + "local_id": 43, + "plane": "intent", + "kind": "criterion", + "title": "When provided a representative artifact fixture (drawn from the project's actual derivation history with N frames, where 5 ≤ N ≤ 15), the m…", + "body": "When provided a representative artifact fixture (drawn from the project's actual derivation history with N frames, where 5 ≤ N ≤ 15), the macro view renders a top-level semantic node count (PhaseGroupNodes + ImpasseNodes) in the range [20, 40]. The rendered edge count remains within an order of magnitude of the node count. Verify with a fixture-based test asserting count bounds.", + "basis": "explicit", + "source": "derived [CR57]", + "detail": null + }, + { + "local_id": 44, + "plane": "intent", + "kind": "criterion", + "title": "A rendered PhaseGroupNode has: (a) computed border-width 1px and border-color resolving to its --color-phase-* token; (b) computed backgrou…", + "body": "A rendered PhaseGroupNode has: (a) computed border-width 1px and border-color resolving to its --color-phase-* token; (b) computed background-color resolving to --color-surface-1; (c) a child element bearing a class or attribute identifying it as a scanline overlay; and (d) a header element containing the phase name, the frame's displayId, and the frame.mode text in --color-text-secondary. Verify with parameterized RTL tests for each phase, asserting computed styles and text content.", + "basis": "explicit", + "source": "derived [CR39]", + "detail": null + }, + { + "local_id": 45, + "plane": "intent", + "kind": "requirement", + "title": "When a phase group ends without any selected PerspectiveNode, the IR builder shall synthesize a PhantomNode under that phase group; the Pha…", + "body": "When a phase group ends without any selected PerspectiveNode, the IR builder shall synthesize a PhantomNode under that phase group; the PhantomNode shall not correspond to any record in ArtifactFile and shall be labelled to indicate 'no perspective taken'.", + "basis": "explicit", + "source": "derived [R15]", + "detail": null + }, + { + "local_id": 46, + "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": 47, + "plane": "intent", + "kind": "requirement", + "title": "PhantomNode instances shall not dispatch any selection action on click and shall not present any other interactive affordance.", + "body": "PhantomNode instances shall not dispatch any selection action on click and shall not present any other interactive affordance.", + "basis": "explicit", + "source": "derived [R31]", + "detail": null + }, + { + "local_id": 48, + "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": 49, + "plane": "intent", + "kind": "criterion", + "title": "A DerivationRunNode for a record with runIndex=3, inputNodeIds.length=5, outputCandidateIds.length=2, impassesFound.length=1 renders DOM co…", + "body": "A DerivationRunNode for a record with runIndex=3, inputNodeIds.length=5, outputCandidateIds.length=2, impassesFound.length=1 renders DOM containing: text matching /RUN\\s*#?\\s*3/, an input badge showing 5, an output badge showing 2, and an impasses-found indicator showing 1. Verify with a single RTL test on a fixture record.", + "basis": "explicit", + "source": "derived [CR42]", + "detail": null + }, + { + "local_id": 50, + "plane": "intent", + "kind": "criterion", + "title": "When a PhaseGroupNode is collapsed, its descendant DerivationRunNode/FanInNode/ReconciliationNode/PerspectiveNode/PhantomNode instances are…", + "body": "When a PhaseGroupNode is collapsed, its descendant DerivationRunNode/FanInNode/ReconciliationNode/PerspectiveNode/PhantomNode instances are absent from the rendered React Flow nodes array (or have hidden:true). Edges whose both endpoints lie inside the collapsed group are absent (or hidden). Verify with a unit test on layout() output asserting child-node-id absence and internal-edge absence.", + "basis": "explicit", + "source": "derived [CR29]", + "detail": null + }, + { + "local_id": 51, + "plane": "intent", + "kind": "requirement", + "title": "Each ImpasseNode shall render with a diamond/lozenge silhouette (distinct from the rectangular phase-group/run/fan-in/reconciliation shapes…", + "body": "Each ImpasseNode shall render with a diamond/lozenge silhouette (distinct from the rectangular phase-group/run/fan-in/reconciliation shapes), a red glyph treatment, and its hub displayId visible on the node face.", + "basis": "explicit", + "source": "derived [R48]", + "detail": null + }, + { + "local_id": 52, + "plane": "intent", + "kind": "criterion", + "title": "Given a fixture containing both a DerivationRunNode with status='failed' AND an ImpasseNode whose linked reconciliation outcome='bail' (the…", + "body": "Given a fixture containing both a DerivationRunNode with status='failed' AND an ImpasseNode whose linked reconciliation outcome='bail' (the dead-end case): both nodes share red+dim treatment, but the ImpasseNode additionally renders a 'DEAD-END' chip while the failed RunNode does not, AND the ImpasseNode renders with the diamond shape while the RunNode is rectangular. Verify with an RTL test rendering both fixtures side by side and asserting these distinguishing features.", + "basis": "explicit", + "source": "derived [CR68]", + "detail": null + }, + { + "local_id": 53, + "plane": "intent", + "kind": "requirement", + "title": "Within a single frame, PhaseGroupNodes shall be stacked vertically in the reverse of PHASE_ORDER (defining_done at top, then pinning, then…", + "body": "Within a single frame, PhaseGroupNodes shall be stacked vertically in the reverse of PHASE_ORDER (defining_done at top, then pinning, then shaping, then grounding at bottom).", + "basis": "explicit", + "source": "derived [R20]", + "detail": null + }, + { + "local_id": 54, + "plane": "intent", + "kind": "criterion", + "title": "After collapsing one or more groups in MacroView, inspecting localStorage, sessionStorage, document.cookie, the URL/location (search/hash),…", + "body": "After collapsing one or more groups in MacroView, inspecting localStorage, sessionStorage, document.cookie, the URL/location (search/hash), and IndexedDB shows no key/entry containing collapsed FrameIds or any macro-view collapse state. After unmount + remount, all groups render expanded again. Verify with a JSDOM test that spies on storage APIs and asserts no setItem call with macro-related keys, and a remount test asserting state reset.", + "basis": "explicit", + "source": "derived [CR24]", + "detail": null + }, + { + "local_id": 55, + "plane": "intent", + "kind": "requirement", + "title": "Impasse-spawn edges shall be rendered as red dashed lines with markerEnd arrows.", + "body": "Impasse-spawn edges shall be rendered as red dashed lines with markerEnd arrows. The source endpoint shall be the ReconciliationNode (or PhaseGroupNode) that produced the impasse, and the target shall be the ImpasseNode that opens the child lane (FrameRecord.triggerImpasseIds).", + "basis": "explicit", + "source": "derived [R53]", + "detail": null + }, + { + "local_id": 56, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall render only three classes of edge between its nodes: (1) sequence edges (RunNode→FanInNode→ReconciliationNode within a…", + "body": "The macro view shall render only three classes of edge between its nodes: (1) sequence edges (RunNode→FanInNode→ReconciliationNode within a phase group), (2) impasse-spawn edges (from a ReconciliationNode/PhaseGroupNode outward to the ImpasseNode opening a child lane), and (3) resolution edges (from a child frame's terminal ReconciliationNode back to the impasse it resolved). It shall not synthesize edges from arbitrary EdgeRecord rows in ArtifactFile.graph.edges.", + "basis": "explicit", + "source": "derived [R51]", + "detail": null + }, + { + "local_id": 57, + "plane": "intent", + "kind": "context", + "title": "The visual treatment of a 'running' run status is underspecified — stakeholders consider it unlikely to appear but want it highlighted some…", + "body": "The visual treatment of a 'running' run status is underspecified — stakeholders consider it unlikely to appear but want it highlighted somehow, leaving the exact treatment open.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK1]", + "detail": null + }, + { + "local_id": 58, + "plane": "intent", + "kind": "requirement", + "title": "After a collapse or expand toggle, sibling nodes' positions shall update so that no dead space remains where the collapsed group's children…", + "body": "After a collapse or expand toggle, sibling nodes' positions shall update so that no dead space remains where the collapsed group's children used to be (sibling reflow). External edges connecting to the collapsed group shall reattach to the pill's bounds without leaving dangling endpoints.", + "basis": "explicit", + "source": "derived [R27]", + "detail": null + }, + { + "local_id": 59, + "plane": "intent", + "kind": "requirement", + "title": "A DerivationRunNode whose status is 'running' shall render with: (a) the existing phosphor-arrive keyframe animation looping at slow tempo…", + "body": "A DerivationRunNode whose status is 'running' shall render with: (a) the existing phosphor-arrive keyframe animation looping at slow tempo on the node body, (b) a 'RUNNING' chip in --color-phosphor-cyan in the header, and (c) an animated scanline sweep across the node interior. The node shall remain clickable and shall display the same content fields as a completed run.", + "basis": "explicit", + "source": "derived [R44]", + "detail": null + }, + { + "local_id": 60, + "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": 61, + "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": 62, + "plane": "intent", + "kind": "term", + "title": "The FrameRecord.nudgingActive flag indicates whether a minimal negative constra…", + "body": null, + "basis": "accepted_review_set", + "source": "technical-inferred [T9]", + "detail": { + "definition": "The FrameRecord.nudgingActive flag indicates whether a minimal negative constraint nudge is active for that frame, relevant to representing non-termination handling." + } + }, + { + "local_id": 63, + "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": 64, + "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": 65, + "plane": "intent", + "kind": "requirement", + "title": "When a PhaseGroupNode is in the collapsed set, the macro view shall render it as a compact pill displaying: a phase color dot, the frame's…", + "body": "When a PhaseGroupNode is in the collapsed set, the macro view shall render it as a compact pill displaying: a phase color dot, the frame's displayId, the run count (e.g., 'n RUNS'), and an outcome glyph derived from the frame's terminal ReconciliationRecord.outcome (✓ accepted, ↺ retry, ↪ recurse, ✗ bail). The pill shall remain clickable to expand it and to open the detail panel.", + "basis": "explicit", + "source": "derived [R26]", + "detail": null + }, + { + "local_id": 66, + "plane": "intent", + "kind": "criterion", + "title": "A directory listing of src/components/macro/ contains at minimum: index.ts, MacroView.tsx, story-ir.ts, layout.ts, and a nodes/ subdirector…", + "body": "A directory listing of src/components/macro/ contains at minimum: index.ts, MacroView.tsx, story-ir.ts, layout.ts, and a nodes/ subdirectory containing PhaseGroupNode.tsx, DerivationRunNode.tsx, FanInNode.tsx, ReconciliationNode.tsx, ImpasseNode.tsx, PerspectiveNode.tsx, and PhantomNode.tsx. Verify by automated filesystem assertion in a unit test (e.g., fs.readdirSync) listing each expected path.", + "basis": "explicit", + "source": "derived [CR1]", + "detail": null + }, + { + "local_id": 67, + "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": 68, + "plane": "intent", + "kind": "requirement", + "title": "When a FrameRecord.nudgingActive is true, the PhaseGroupNode for that frame shall display a textual chip (e.g., 'NUDGING' or '⚡ NUDGE') sty…", + "body": "When a FrameRecord.nudgingActive is true, the PhaseGroupNode for that frame shall display a textual chip (e.g., 'NUDGING' or '⚡ NUDGE') styled in --color-phosphor-amber inside the node header. The nudging indicator shall be inside the node body, not an external overlay.", + "basis": "explicit", + "source": "derived [R40]", + "detail": null + }, + { + "local_id": 69, + "plane": "intent", + "kind": "requirement", + "title": "Faded (rejected/unselected) PerspectiveNode branches shall not dispatch any selection action on click and shall present no interactive affo…", + "body": "Faded (rejected/unselected) PerspectiveNode branches shall not dispatch any selection action on click and shall present no interactive affordances.", + "basis": "explicit", + "source": "derived [R32]", + "detail": null + }, + { + "local_id": 70, + "plane": "intent", + "kind": "requirement", + "title": "Each FanInNode shall render its FanIn groupings as a stack of rows, one row per grouping, where each row is prefixed by a 4px-wide left bor…", + "body": "Each FanInNode shall render its FanIn groupings as a stack of rows, one row per grouping, where each row is prefixed by a 4px-wide left border colored: green (success/merged token) when grouping.resolution='merged', amber (--color-phosphor-amber) when grouping.resolution='best_selected', and red (--color-phosphor-red) when grouping.resolution='impasse_surfaced'. Each row shall display the grouping's groupKey and a node count.", + "basis": "explicit", + "source": "derived [R45]", + "detail": null + }, + { + "local_id": 71, + "plane": "intent", + "kind": "requirement", + "title": "A DerivationRunNode whose status is 'completed' shall render with the base node treatment (no special border or dimming).", + "body": "A DerivationRunNode whose status is 'completed' shall render with the base node treatment (no special border or dimming).", + "basis": "explicit", + "source": "derived [R42]", + "detail": null + }, + { + "local_id": 72, + "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": 73, + "plane": "intent", + "kind": "requirement", + "title": "The Story IR and layout modules shall consume the branded ID types defined in src/types/artifact.ts (NodeId, EdgeId, FrameId, RunId, FanInI…", + "body": "The Story IR and layout modules shall consume the branded ID types defined in src/types/artifact.ts (NodeId, EdgeId, FrameId, RunId, FanInId, ReconciliationId, etc.) for keying records and shall not coerce these to plain strings at module boundaries.", + "basis": "explicit", + "source": "derived [R58]", + "detail": null + }, + { + "local_id": 74, + "plane": "intent", + "kind": "requirement", + "title": "The layout shall assign each frame to a horizontal lane indexed by its derivationDepth, with depth 0 acting as the trunk and each increment…", + "body": "The layout shall assign each frame to a horizontal lane indexed by its derivationDepth, with depth 0 acting as the trunk and each increment opening a lane to the right (or otherwise increasing horizontal breadth) so that onion-peel depth is encoded in horizontal position.", + "basis": "explicit", + "source": "derived [R17]", + "detail": null + }, + { + "local_id": 75, + "plane": "intent", + "kind": "requirement", + "title": "The story-ir / layout shall emit one PhaseGroupNode per (FrameRecord, Phase) pair that has any associated runs, fan-in, reconciliation, or…", + "body": "The story-ir / layout shall emit one PhaseGroupNode per (FrameRecord, Phase) pair that has any associated runs, fan-in, reconciliation, or perspective records.", + "basis": "explicit", + "source": "derived [R11]", + "detail": null + }, + { + "local_id": 76, + "plane": "intent", + "kind": "requirement", + "title": "All textual content rendered inside macro view nodes, edges, banner, and pills shall use the var(--font-mono) (JetBrains Mono) font stack d…", + "body": "All textual content rendered inside macro view nodes, edges, banner, and pills shall use the var(--font-mono) (JetBrains Mono) font stack defined in theme.css.", + "basis": "explicit", + "source": "derived [R35]", + "detail": null + }, + { + "local_id": 77, + "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": 78, + "plane": "intent", + "kind": "criterion", + "title": "For every edge in the layout output (in the default, non-running state), edge.animated is falsy (undefined or false).", + "body": "For every edge in the layout output (in the default, non-running state), edge.animated is falsy (undefined or false). Verify by a unit-test assertion across all edges in a representative fixture.", + "basis": "explicit", + "source": "derived [CR56]", + "detail": null + }, + { + "local_id": 79, + "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": 80, + "plane": "intent", + "kind": "requirement", + "title": "The MacroView shall provide a manual 'RELOAD' button that, when clicked, re-runs the entire data load → IR → layout → render pipeline.", + "body": "The MacroView shall provide a manual 'RELOAD' button that, when clicked, re-runs the entire data load → IR → layout → render pipeline.", + "basis": "explicit", + "source": "derived [R6]", + "detail": null + }, + { + "local_id": 81, + "plane": "intent", + "kind": "criterion", + "title": "A PerspectiveNode with perspectiveStatus='selected' renders with computed CSS opacity == 1.0 (or no opacity rule reducing it).", + "body": "A PerspectiveNode with perspectiveStatus='selected' renders with computed CSS opacity == 1.0 (or no opacity rule reducing it). A PerspectiveNode with perspectiveStatus='rejected' or 'open' (non-taken) renders with computed CSS opacity in the range [0.25, 0.35] (target ~0.3). Verify with RTL + getComputedStyle assertions on parameterized fixtures.", + "basis": "explicit", + "source": "derived [CR34]", + "detail": null + }, + { + "local_id": 82, + "plane": "intent", + "kind": "criterion", + "title": "The nodeTypes object passed to contains exactly the seven keys: PhaseGroupNode, DerivationRunNode, FanInNode, ReconciliationNod…", + "body": "The nodeTypes object passed to contains exactly the seven keys: PhaseGroupNode, DerivationRunNode, FanInNode, ReconciliationNode, ImpasseNode, PerspectiveNode, PhantomNode (or 'phaseGroup','run','fanIn','reconciliation','impasse','perspective','phantom' equivalents) and no key matching /trunk/i. Verify with a unit test that imports the nodeTypes registry and asserts Object.keys length === 7 and contains the expected set.", + "basis": "explicit", + "source": "derived [CR11]", + "detail": null + }, + { + "local_id": 83, + "plane": "intent", + "kind": "requirement", + "title": "When a ReconciliationRecord.materialProgress is true, the corresponding ReconciliationNode shall render a small ✓ chip beside the outcome c…", + "body": "When a ReconciliationRecord.materialProgress is true, the corresponding ReconciliationNode shall render a small ✓ chip beside the outcome chip in its header.", + "basis": "explicit", + "source": "derived [R47]", + "detail": null + }, + { + "local_id": 84, + "plane": "intent", + "kind": "requirement", + "title": "The existing src/components/MacroView.tsx import path shall continue to resolve to a working MacroView component (e.g., as a thin re-export…", + "body": "The existing src/components/MacroView.tsx import path shall continue to resolve to a working MacroView component (e.g., as a thin re-export of the new src/components/macro/ module) so that routes/explore.tsx and other existing importers do not require changes.", + "basis": "explicit", + "source": "derived [R2]", + "detail": null + }, + { + "local_id": 85, + "plane": "intent", + "kind": "criterion", + "title": "package.json dependencies and devDependencies contain no entry for 'dagre', '@dagrejs/dagre', 'elkjs', 'cytoscape', 'klay', or other genera…", + "body": "package.json dependencies and devDependencies contain no entry for 'dagre', '@dagrejs/dagre', 'elkjs', 'cytoscape', 'klay', or other general-purpose graph layout libraries. layout.ts contains no imports from such packages. Verify by a static test asserting the dependency lists and import set.", + "basis": "explicit", + "source": "derived [CR21]", + "detail": null + }, + { + "local_id": 86, + "plane": "intent", + "kind": "requirement", + "title": "Macro view components shall not use generic Material Design components, default Tailwind component patterns, generic SaaS dashboard chrome,…", + "body": "Macro view components shall not use generic Material Design components, default Tailwind component patterns, generic SaaS dashboard chrome, generic rounded-corner card aesthetics, or blue primary buttons. Visual treatments shall instead be expressed through the CRT/phosphor visual grammar (warm dark surfaces, phosphor glow, scanline texture, sharp/squared edges, monospace typography).", + "basis": "explicit", + "source": "derived [R36]", + "detail": null + }, + { + "local_id": 87, + "plane": "intent", + "kind": "criterion", + "title": "When FrameRecord.nudgingActive is true, the PhaseGroupNode header contains a chip element whose textContent matches /NUDG(E|ING)/ and whose…", + "body": "When FrameRecord.nudgingActive is true, the PhaseGroupNode header contains a chip element whose textContent matches /NUDG(E|ING)/ and whose computed color resolves to --color-phosphor-amber. The chip is a descendant of the node body (i.e., bounded inside the node's rect), not an external overlay. When nudgingActive is false, no such chip is present. Verify with two RTL fixtures.", + "basis": "explicit", + "source": "derived [CR41]", + "detail": null + }, + { + "local_id": 88, + "plane": "intent", + "kind": "requirement", + "title": "Phase color references in macro view code shall use the theme.css phase tokens (--color-phase-grounding, --color-phase-shaping, --color-pha…", + "body": "Phase color references in macro view code shall use the theme.css phase tokens (--color-phase-grounding, --color-phase-shaping, --color-phase-pinning, --color-phase-defining-done) as the authoritative phase color mapping. Where the upstream X34 specification differs from theme.css, theme.css governs.", + "basis": "explicit", + "source": "derived [R37]", + "detail": null + }, + { + "local_id": 89, + "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": 90, + "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": 91, + "plane": "intent", + "kind": "criterion", + "title": "In the layout output, every node with a defined parentId has node.position expressed relative to the parent's origin (i.e., the absolute sc…", + "body": "In the layout output, every node with a defined parentId has node.position expressed relative to the parent's origin (i.e., the absolute screen position is parent.position + child.position), and the child's position is contained within the parent's bounding box (0 ≤ child.x ≤ parent.width - child.width; same for y). Group nodes (parentId undefined) carry absolute positions. Verify with a unit test on layout output for a fixture containing parented children.", + "basis": "explicit", + "source": "derived [CR62]", + "detail": null + }, + { + "local_id": 92, + "plane": "intent", + "kind": "criterion", + "title": "In the layout output for any non-collapsed PhaseGroupNode P, every child DerivationRunNode/FanInNode/ReconciliationNode/PerspectiveNode in…", + "body": "In the layout output for any non-collapsed PhaseGroupNode P, every child DerivationRunNode/FanInNode/ReconciliationNode/PerspectiveNode in P has node.parentId === P.id and node.extent === 'parent', and P has type === 'group' (or the React Flow group equivalent). Verify with a unit test on layout() output asserting these properties for a fixture frame containing all four child kinds.", + "basis": "explicit", + "source": "derived [CR13]", + "detail": null + }, + { + "local_id": 93, + "plane": "intent", + "kind": "requirement", + "title": "Within any horizontal lane, more recent frames (higher attemptNumber / later createdAt) shall be positioned higher (smaller y in screen coo…", + "body": "Within any horizontal lane, more recent frames (higher attemptNumber / later createdAt) shall be positioned higher (smaller y in screen coordinates / 'higher' visually) than earlier frames, so that vertical position encodes time with t+1 above t.", + "basis": "explicit", + "source": "derived [R18]", + "detail": null + }, + { + "local_id": 94, + "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": 95, + "plane": "intent", + "kind": "criterion", + "title": "Manual UX review (codified as a stakeholder sign-off checklist) asserts: (a) every node communicates outcome-at-a-glance from at least 1m v…", + "body": "Manual UX review (codified as a stakeholder sign-off checklist) asserts: (a) every node communicates outcome-at-a-glance from at least 1m viewing distance on a 14\" laptop screen at default zoom; (b) no rendered phase group exceeds ~280px width or ~360px height at default zoom for typical content; (c) text contrast against warm-dark surface meets WCAG AA (4.5:1) for primary text. Verify with a manual review checklist run during PR review plus an automated contrast test using getComputedStyle and a contrast-ratio library.", + "basis": "explicit", + "source": "derived [CR69]", + "detail": null + }, + { + "local_id": 96, + "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": 97, + "plane": "intent", + "kind": "requirement", + "title": "Layout output for child nodes inside a PhaseGroupNode shall use positions relative to the parent group's origin (consistent with React Flow…", + "body": "Layout output for child nodes inside a PhaseGroupNode shall use positions relative to the parent group's origin (consistent with React Flow's parentId conventions), while group nodes themselves carry absolute positions.", + "basis": "explicit", + "source": "derived [R62]", + "detail": null + }, + { + "local_id": 98, + "plane": "intent", + "kind": "criterion", + "title": "For a fixture with frames F0 (no parent) → F1 (parent F0) → F2 (parent F1), the layout assigns depth(F0)=0, depth(F1)=1, depth(F2)=2; and t…", + "body": "For a fixture with frames F0 (no parent) → F1 (parent F0) → F2 (parent F1), the layout assigns depth(F0)=0, depth(F1)=1, depth(F2)=2; and the x-coordinate of F2's PhaseGroupNode is greater than F1's, which is greater than F0's. Verify with a unit test on layout() output.", + "basis": "explicit", + "source": "derived [CR17]", + "detail": null + }, + { + "local_id": 99, + "plane": "intent", + "kind": "context", + "title": "Because the macro view is snapshot-only and only updates on manual refresh, users may view a stale derivation history without realizing it.", + "body": "Because the macro view is snapshot-only and only updates on manual refresh, users may view a stale derivation history without realizing it.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK3]", + "detail": null + }, + { + "local_id": 100, + "plane": "intent", + "kind": "requirement", + "title": "The MacroView shall implement a three-stage pipeline as separate, individually-testable pure functions: (1) story-ir builder consuming Arti…", + "body": "The MacroView shall implement a three-stage pipeline as separate, individually-testable pure functions: (1) story-ir builder consuming ArtifactFile.graph and producing a normalized derivation tree IR, (2) a layout function consuming the IR plus a collapsed set and producing absolute positions, lane widths and parent/child grouping, and (3) a renderer that maps IR nodes to typed React Flow nodes and edges. Stages 1 and 2 shall have no React or React Flow imports.", + "basis": "explicit", + "source": "derived [R4]", + "detail": null + }, + { + "local_id": 101, + "plane": "intent", + "kind": "requirement", + "title": "Each DerivationRunNode shall display: the run index ('RUN #n' from runIndex), an input-count badge (size of inputNodeIds), an output-count…", + "body": "Each DerivationRunNode shall display: the run index ('RUN #n' from runIndex), an input-count badge (size of inputNodeIds), an output-count badge (size of outputCandidateIds), and an impasses-found count (size of impassesFound).", + "basis": "explicit", + "source": "derived [R41]", + "detail": null + }, + { + "local_id": 102, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall be implemented as React components in TypeScript that build cleanly within the existing Vite + React + Tailwind SPA to…", + "body": "The macro view shall be implemented as React components in TypeScript that build cleanly within the existing Vite + React + Tailwind SPA toolchain, without introducing alternative bundlers, runtimes, or replacing Tailwind with a competing CSS framework.", + "basis": "explicit", + "source": "derived [R60]", + "detail": null + }, + { + "local_id": 103, + "plane": "oracle", + "kind": "evidence", + "title": "The ReconciliationRecord.outcome field can be one of: accepted, retry, recurse, or bail.", + "body": "The ReconciliationRecord.outcome field can be one of: accepted, retry, recurse, or bail.", + "basis": "explicit", + "source": "technical-observed [E3]", + "detail": null + }, + { + "local_id": 104, + "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": 105, + "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": 106, + "plane": "intent", + "kind": "requirement", + "title": "Clicking the collapse/expand affordance on a PhaseGroupNode (or its collapsed pill) shall toggle that group's membership in the collapsed s…", + "body": "Clicking the collapse/expand affordance on a PhaseGroupNode (or its collapsed pill) shall toggle that group's membership in the collapsed set, triggering a synchronous re-run of the layout function over the existing IR + new collapsed set.", + "basis": "explicit", + "source": "derived [R25]", + "detail": null + }, + { + "local_id": 107, + "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": 108, + "plane": "intent", + "kind": "criterion", + "title": "An edge whose source or target is a child of a now-collapsed phase group renders in the layout output with its endpoint id rewritten to (or…", + "body": "An edge whose source or target is a child of a now-collapsed phase group renders in the layout output with its endpoint id rewritten to (or remapped to terminate at) the collapsed pill node id, not the original child id. No edge in the output references a child id that is currently hidden by a collapsed group. Verify with a unit test on layout() output asserting endpoint-id sets are subsets of the visible node-id set.", + "basis": "explicit", + "source": "derived [CR28]", + "detail": null + }, + { + "local_id": 109, + "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": 110, + "plane": "intent", + "kind": "criterion", + "title": "Every text-bearing DOM element rendered by macro view components has computed font-family containing 'JetBrains Mono' or resolving to var(-…", + "body": "Every text-bearing DOM element rendered by macro view components has computed font-family containing 'JetBrains Mono' or resolving to var(--font-mono). Verify with an RTL test that walks the rendered tree and asserts getComputedStyle(el).fontFamily for each text element matches the expected stack.", + "basis": "explicit", + "source": "derived [CR36]", + "detail": null + }, + { + "local_id": 111, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall register exactly seven custom React Flow nodeTypes — PhaseGroupNode, DerivationRunNode, FanInNode, ReconciliationNode,…", + "body": "The macro view shall register exactly seven custom React Flow nodeTypes — PhaseGroupNode, DerivationRunNode, FanInNode, ReconciliationNode, ImpasseNode, PerspectiveNode, and PhantomNode — and shall not register a separate TrunkNode type.", + "basis": "explicit", + "source": "derived [R10]", + "detail": null + }, + { + "local_id": 112, + "plane": "intent", + "kind": "criterion", + "title": "The macro view module imports DetailPanel from src/components/DetailPanel.tsx (or via the shared component path) and does not define a new…", + "body": "The macro view module imports DetailPanel from src/components/DetailPanel.tsx (or via the shared component path) and does not define a new component named MacroDetailPanel or equivalent. DetailPanel renders the selected macro record kind. Verify with (a) a static import-graph check and (b) an integration test that selects each macro record kind and asserts DetailPanel renders kind-appropriate content.", + "basis": "explicit", + "source": "derived [CR31]", + "detail": null + }, + { + "local_id": 113, + "plane": "intent", + "kind": "requirement", + "title": "The macro view source code shall be organized under src/components/macro/ with at minimum: index.ts re-exporting MacroView, MacroView.tsx (…", + "body": "The macro view source code shall be organized under src/components/macro/ with at minimum: index.ts re-exporting MacroView, MacroView.tsx (top-level component), story-ir.ts (pure ArtifactFile→StoryIR builder), layout.ts (pure StoryIR+collapsedSet→RF nodes/edges), and a nodes/ subdirectory containing one .tsx file per custom node type (PhaseGroupNode, DerivationRunNode, FanInNode, ReconciliationNode, ImpasseNode, PerspectiveNode, PhantomNode).", + "basis": "explicit", + "source": "derived [R1]", + "detail": null + }, + { + "local_id": 114, + "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": 115, + "plane": "intent", + "kind": "criterion", + "title": "After collapsing one or more groups and triggering RELOAD (or unmount/remount), the rendered macro view returns to the fully-expanded state…", + "body": "After collapsing one or more groups and triggering RELOAD (or unmount/remount), the rendered macro view returns to the fully-expanded state with no PhaseGroupNode rendered as a pill. Verify with an RTL test that collapses, reloads, and asserts no collapsed-pill DOM elements exist.", + "basis": "explicit", + "source": "derived [CR67]", + "detail": null + }, + { + "local_id": 116, + "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": 117, + "plane": "intent", + "kind": "criterion", + "title": "Inspecting MacroView's rendered DOM and the props of its React Flow custom nodes reveals no UI affordance bound to a mutating action: no ad…", + "body": "Inspecting MacroView's rendered DOM and the props of its React Flow custom nodes reveals no UI affordance bound to a mutating action: no add/edit/delete buttons, no form inputs, no draggable-to-create-edge handles enabled (nodesDraggable may be true for layout, but onConnect/onEdgesChange handlers must not commit changes back to the artifact). Verify by an integration test that simulates clicks on every interactive element and asserts that no mock 'mutate' API on the store is ever called.", + "basis": "explicit", + "source": "derived [CR9]", + "detail": null + }, + { + "local_id": 118, + "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": 119, + "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": 120, + "plane": "intent", + "kind": "requirement", + "title": "When a PhaseGroupNode is collapsed, its child DerivationRunNode/FanInNode/ReconciliationNode/PerspectiveNode/PhantomNode descendants and th…", + "body": "When a PhaseGroupNode is collapsed, its child DerivationRunNode/FanInNode/ReconciliationNode/PerspectiveNode/PhantomNode descendants and the edges entirely internal to that group shall not be rendered (or shall be hidden from the React Flow output).", + "basis": "explicit", + "source": "derived [R28]", + "detail": null + }, + { + "local_id": 121, + "plane": "intent", + "kind": "requirement", + "title": "The MacroView shall expose no controls that mutate artifact data.", + "body": "The MacroView shall expose no controls that mutate artifact data. Only pan, zoom, node click (selection), and collapse/expand interactions shall be wired.", + "basis": "explicit", + "source": "derived [R8]", + "detail": null + }, + { + "local_id": 122, + "plane": "oracle", + "kind": "evidence", + "title": "The artifact.ts file defines branded ID types for NodeId, EdgeId, FrameId, SpecId, SourceId, ClaimId, RunId, FanInId, ReconciliationId, Sna…", + "body": "The artifact.ts file defines branded ID types for NodeId, EdgeId, FrameId, SpecId, SourceId, ClaimId, RunId, FanInId, ReconciliationId, SnapshotId, InterventionId, and DisplayId.", + "basis": "explicit", + "source": "technical-observed [E2]", + "detail": null + }, + { + "local_id": 123, + "plane": "intent", + "kind": "criterion", + "title": "Git diff between the feature branch and main shows zero modifications to src/components/MicroView*, src/graph/, src/router.ts, src/routes/,…", + "body": "Git diff between the feature branch and main shows zero modifications to src/components/MicroView*, src/graph/, src/router.ts, src/routes/, or any unrelated component, with the only allowed touched files being: src/components/macro/**, src/components/MacroView.tsx (re-export only), and minimal additions to src/components/DetailPanel.tsx (new branches for macro record kinds; no removal of existing branches). Verify with a git-diff CI check.", + "basis": "explicit", + "source": "derived [CR59]", + "detail": null + }, + { + "local_id": 124, + "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": 125, + "plane": "intent", + "kind": "criterion", + "title": "After mount, mutating the underlying artifact mock and waiting any reasonable interval (e.g., 1s) does NOT change the rendered macro view (…", + "body": "After mount, mutating the underlying artifact mock and waiting any reasonable interval (e.g., 1s) does NOT change the rendered macro view (no live update). Clicking the RELOAD button (or remounting) updates the rendered view to reflect the mutation. Verify with an integration test combining a mock artifact source, mutation between assertions, and pre/post-reload DOM snapshots.", + "basis": "explicit", + "source": "derived [CR66]", + "detail": null + }, + { + "local_id": 126, + "plane": "intent", + "kind": "criterion", + "title": "A FanInNode with groupings [{groupKey:'a', resolution:'merged', nodeCount:3},{groupKey:'b', resolution:'best_selected', nodeCount:1},{group…", + "body": "A FanInNode with groupings [{groupKey:'a', resolution:'merged', nodeCount:3},{groupKey:'b', resolution:'best_selected', nodeCount:1},{groupKey:'c', resolution:'impasse_surfaced', nodeCount:2}] renders three row elements in order, each with a 4px-wide left-border whose color is (in order): the green/merged token, --color-phosphor-amber, --color-phosphor-red. Each row contains the groupKey text and the nodeCount text. Verify with a parameterized RTL test across all three resolution kinds.", + "basis": "explicit", + "source": "derived [CR46]", + "detail": null + }, + { + "local_id": 127, + "plane": "intent", + "kind": "criterion", + "title": "Given a fixture artifact containing N FrameRecords each with M phases that have at least one associated run/fan-in/reconciliation/perspecti…", + "body": "Given a fixture artifact containing N FrameRecords each with M phases that have at least one associated run/fan-in/reconciliation/perspective record, the IR builder produces exactly N×M PhaseGroupNodes (no more, no fewer), and zero PhaseGroupNodes for (frame, phase) pairs with no associated records. Verify with a unit test on buildStoryIR using a hand-crafted fixture covering empty and non-empty phase pairs.", + "basis": "explicit", + "source": "derived [CR12]", + "detail": null + }, + { + "local_id": 128, + "plane": "intent", + "kind": "requirement", + "title": "The macro view canvas shall support pan and zoom interactions provided by React Flow.", + "body": "The macro view canvas shall support pan and zoom interactions provided by React Flow.", + "basis": "explicit", + "source": "derived [R9]", + "detail": null + }, + { + "local_id": 129, + "plane": "intent", + "kind": "requirement", + "title": "PhaseGroupNode shall encode the FrameRecord.mode in border style: mode='initial' uses a solid border, mode='rederive' uses a double border,…", + "body": "PhaseGroupNode shall encode the FrameRecord.mode in border style: mode='initial' uses a solid border, mode='rederive' uses a double border, and mode='grounding_enrichment' uses a dashed border. A small text mode chip showing the mode name shall additionally appear in the header.", + "basis": "explicit", + "source": "derived [R39]", + "detail": null + }, + { + "local_id": 130, + "plane": "intent", + "kind": "criterion", + "title": "For an artifact fixture with arbitrary counts of DerivationRunRecord, FanInRecord, and ReconciliationRecord, the rendered React Flow node a…", + "body": "For an artifact fixture with arbitrary counts of DerivationRunRecord, FanInRecord, and ReconciliationRecord, the rendered React Flow node array contains exactly one DerivationRunNode per DerivationRunRecord.id, one FanInNode per FanInRecord.id, and one ReconciliationNode per ReconciliationRecord.id (verified by id-set equality). Verify with a property-based test (fast-check) generating random combinations.", + "basis": "explicit", + "source": "derived [CR14]", + "detail": null + }, + { + "local_id": 131, + "plane": "intent", + "kind": "requirement", + "title": "The MacroView shall display a fixed overlay banner in the top-left corner showing 'SNAPSHOT @ ' rendered in --color-text-tertiar…", + "body": "The MacroView shall display a fixed overlay banner in the top-left corner showing 'SNAPSHOT @ ' rendered in --color-text-tertiary, where reflects the time the artifact was loaded for the current snapshot. The banner shall not pan or zoom with the React Flow canvas.", + "basis": "explicit", + "source": "derived [R7]", + "detail": null + }, + { + "local_id": 132, + "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": 133, + "plane": "intent", + "kind": "criterion", + "title": "MacroView renders a fixed-position element in the top-left containing the literal text prefix 'SNAPSHOT @ ' followed by the artifact's load…", + "body": "MacroView renders a fixed-position element in the top-left containing the literal text prefix 'SNAPSHOT @ ' followed by the artifact's load timestamp. The element has CSS color resolving to the value of --color-text-tertiary and CSS position:fixed (or absolute relative to the macro view container, outside the React Flow viewport transform). Verify by computed-style assertion in a JSDOM/RTL test, plus a visual test that pans/zooms the canvas and asserts the banner's bounding-rect coordinates are unchanged.", + "basis": "explicit", + "source": "derived [CR8]", + "detail": null + }, + { + "local_id": 134, + "plane": "intent", + "kind": "requirement", + "title": "Each ReconciliationNode shall encode its outcome via full-node border treatment: outcome='accepted' uses the parent phase's color, outcome=…", + "body": "Each ReconciliationNode shall encode its outcome via full-node border treatment: outcome='accepted' uses the parent phase's color, outcome='retry' uses --color-phosphor-amber, outcome='recurse' uses --color-phosphor-cyan (blue), outcome='bail' uses --color-phosphor-red plus a dimmed interior. The outcome shall additionally appear as a textual chip in the node header.", + "basis": "explicit", + "source": "derived [R46]", + "detail": null + }, + { + "local_id": 135, + "plane": "intent", + "kind": "criterion", + "title": "Every reference to a phase color in macro view source uses one of the literal tokens --color-phase-grounding, --color-phase-shaping, --colo…", + "body": "Every reference to a phase color in macro view source uses one of the literal tokens --color-phase-grounding, --color-phase-shaping, --color-phase-pinning, or --color-phase-defining-done. No macro view file redefines these tokens. Where conflict exists between X34 and theme.css, theme.css's mapping is used. Verify with a grep test plus a render test asserting that a PhaseGroupNode for phase 'shaping' has border-color resolving to theme.css's --color-phase-shaping value (currently amber per X41).", + "basis": "explicit", + "source": "derived [CR38]", + "detail": null + }, + { + "local_id": 136, + "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": 137, + "plane": "intent", + "kind": "requirement", + "title": "Sequence edges (RunNode→FanInNode→ReconciliationNode) shall be rendered as thin amber lines with markerEnd arrows and no animation by defau…", + "body": "Sequence edges (RunNode→FanInNode→ReconciliationNode) shall be rendered as thin amber lines with markerEnd arrows and no animation by default.", + "basis": "explicit", + "source": "derived [R52]", + "detail": null + }, + { + "local_id": 138, + "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": 139, + "plane": "intent", + "kind": "criterion", + "title": "A static review (codified as a test fixture matrix) asserts the channel-to-attribute mapping: phase color used only for phase identity (not…", + "body": "A static review (codified as a test fixture matrix) asserts the channel-to-attribute mapping: phase color used only for phase identity (not for outcome or mode); border-color encoding only run/reconciliation outcome semantics (red, amber, cyan, phase color per outcome); border-style (solid/double/dashed) encoding only frame mode; opacity reduction (~30%) used only for unselected perspective branches; diamond/lozenge shape used only by ImpasseNode. Verify by enumerating all node-type × visual-channel pairs in tests and asserting no two attributes share a channel within a node.", + "basis": "explicit", + "source": "derived [CR64]", + "detail": null + }, + { + "local_id": 140, + "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": 141, + "plane": "intent", + "kind": "requirement", + "title": "For each FrameRecord, the layout shall compute derivationDepth as the length of the parentFrameId chain (the root frame, with no parentFram…", + "body": "For each FrameRecord, the layout shall compute derivationDepth as the length of the parentFrameId chain (the root frame, with no parentFrameId, has depth 0).", + "basis": "explicit", + "source": "derived [R16]", + "detail": null + }, + { + "local_id": 142, + "plane": "oracle", + "kind": "evidence", + "title": "The FrameRecord.mode field has three values: initial, rederive, and grounding_enrichment.", + "body": "The FrameRecord.mode field has three values: initial, rederive, and grounding_enrichment.", + "basis": "explicit", + "source": "technical-observed [E4]", + "detail": null + }, + { + "local_id": 143, + "plane": "intent", + "kind": "criterion", + "title": "story-ir.ts and layout.ts modules contain no import statements referencing 'react', 'react-dom', '@xyflow/react', or any DOM/browser API.", + "body": "story-ir.ts and layout.ts modules contain no import statements referencing 'react', 'react-dom', '@xyflow/react', or any DOM/browser API. Verify by a static-analysis test that parses the files and asserts the import set is disjoint from a forbidden list. Additionally call each function twice with the same deeply-cloned input and assert the outputs are deeply equal and that the inputs are unmodified (input integrity hash unchanged).", + "basis": "explicit", + "source": "derived [CR4]", + "detail": null + }, + { + "local_id": 144, + "plane": "intent", + "kind": "criterion", + "title": "Clicking the collapse affordance on an expanded PhaseGroupNode causes (a) that group's id to enter the collapsed set and (b) the layout fun…", + "body": "Clicking the collapse affordance on an expanded PhaseGroupNode causes (a) that group's id to enter the collapsed set and (b) the layout function to be invoked again with the new set, producing updated node positions. Subsequent click on the resulting pill removes the id from the set and restores expanded layout. Verify with a Vitest test using a spy on the layout function and a click simulation; assert layout invocation count and node-position diffs.", + "basis": "explicit", + "source": "derived [CR25]", + "detail": null + }, + { + "local_id": 145, + "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": 146, + "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": 147, + "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": 148, + "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": 149, + "plane": "intent", + "kind": "requirement", + "title": "On every mount of MacroView the collapsed-set shall be initialized as empty, so that all phase groups are rendered fully expanded immediate…", + "body": "On every mount of MacroView the collapsed-set shall be initialized as empty, so that all phase groups are rendered fully expanded immediately after page load.", + "basis": "explicit", + "source": "derived [R23]", + "detail": null + }, + { + "local_id": 150, + "plane": "intent", + "kind": "criterion", + "title": "A PhantomNode renders with computed border-style 'dashed', computed background-color of 'transparent' (or rgba alpha 0), and contains text…", + "body": "A PhantomNode renders with computed border-style 'dashed', computed background-color of 'transparent' (or rgba alpha 0), and contains text matching /PHANTOM/i and /no perspective taken/i. Verify with an RTL + computed-style test.", + "basis": "explicit", + "source": "derived [CR51]", + "detail": null + }, + { + "local_id": 151, + "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": 152, + "plane": "intent", + "kind": "criterion", + "title": "TypeScript strict-mode compilation succeeds for story-ir.ts and layout.ts using the branded ID types from src/types/artifact.ts (FrameId, R…", + "body": "TypeScript strict-mode compilation succeeds for story-ir.ts and layout.ts using the branded ID types from src/types/artifact.ts (FrameId, RunId, FanInId, ReconciliationId, NodeId, etc.) without `as string` or `as any` coercions at module exports. Verify with `tsc --noEmit` in CI plus a grep test asserting no `as string` or `as unknown as string` patterns appear at module boundaries.", + "basis": "explicit", + "source": "derived [CR58]", + "detail": null + }, + { + "local_id": 153, + "plane": "intent", + "kind": "requirement", + "title": "Resolution edges shall be rendered as solid lines colored by the resolving phase's color, with markerEnd arrows, drawn from a child frame's…", + "body": "Resolution edges shall be rendered as solid lines colored by the resolving phase's color, with markerEnd arrows, drawn from a child frame's terminal ReconciliationNode back toward the ImpasseNode listed in that record's resolvedImpasseIds, using a return-leftward routing convention (toward lower-depth lanes).", + "basis": "explicit", + "source": "derived [R54]", + "detail": null + }, + { + "local_id": 154, + "plane": "intent", + "kind": "criterion", + "title": "When ReconciliationRecord.materialProgress is true, the rendered ReconciliationNode header contains a chip element whose textContent contai…", + "body": "When ReconciliationRecord.materialProgress is true, the rendered ReconciliationNode header contains a chip element whose textContent contains the ✓ character (or is identifiable as a checkmark indicator) located beside the outcome chip. When materialProgress is false, no such chip is present. Verify with two RTL fixtures.", + "basis": "explicit", + "source": "derived [CR48]", + "detail": null + }, + { + "local_id": 155, + "plane": "intent", + "kind": "requirement", + "title": "Across realistic spec snapshots the macro view shall render approximately 20–40 top-level semantic nodes (phase groups + impasses) for a ty…", + "body": "Across realistic spec snapshots the macro view shall render approximately 20–40 top-level semantic nodes (phase groups + impasses) for a typical derivation history; the design shall not produce hundreds of nodes from EdgeRecord-style content edges.", + "basis": "explicit", + "source": "derived [R57]", + "detail": null + }, + { + "local_id": 156, + "plane": "intent", + "kind": "criterion", + "title": "Within a single lane, given two frames F_old (createdAt=t1, attemptNumber=1) and F_new (createdAt=t2>t1, attemptNumber=2) at the same depth…", + "body": "Within a single lane, given two frames F_old (createdAt=t1, attemptNumber=1) and F_new (createdAt=t2>t1, attemptNumber=2) at the same depth, F_new's PhaseGroupNode position.y is strictly less than F_old's (smaller y = visually higher). Verify with a unit-test fixture and assertion on layout output.", + "basis": "explicit", + "source": "derived [CR18]", + "detail": null + }, + { + "local_id": 157, + "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": 158, + "plane": "intent", + "kind": "criterion", + "title": "For each FrameRecord with non-empty triggerImpasseIds, layout emits an impasse-spawn edge from the parent frame's relevant ReconciliationNo…", + "body": "For each FrameRecord with non-empty triggerImpasseIds, layout emits an impasse-spawn edge from the parent frame's relevant ReconciliationNode (or PhaseGroupNode if reconciliation is unavailable) to each ImpasseNode listed in triggerImpasseIds. Each such edge has computed stroke color resolving to --color-phosphor-red, computed border/stroke style 'dashed', and a markerEnd arrow. Verify with a fixture and computed-style assertion.", + "basis": "explicit", + "source": "derived [CR54]", + "detail": null + }, + { + "local_id": 159, + "plane": "intent", + "kind": "context", + "title": "Because collapsed state is ephemeral and never persisted, users lose any custom collapsed configuration on every page reload.", + "body": "Because collapsed state is ephemeral and never persisted, users lose any custom collapsed configuration on every page reload.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK4]", + "detail": null + }, + { + "local_id": 160, + "plane": "intent", + "kind": "criterion", + "title": "Clicking a non-faded, non-phantom macro node (frame, run, fan-in, reconciliation, impasse, or selected perspective) dispatches a 'select' a…", + "body": "Clicking a non-faded, non-phantom macro node (frame, run, fan-in, reconciliation, impasse, or selected perspective) dispatches a 'select' action to the global selection store with a payload identifying the underlying IR record by id and kind. Verify with an RTL test using a mocked store: simulate click on each node-type variant in a fixture and assert the store.select spy was called with the correct {id, kind} pair.", + "basis": "explicit", + "source": "derived [CR30]", + "detail": null + }, + { + "local_id": 161, + "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": 162, + "plane": "intent", + "kind": "criterion", + "title": "Layout output contains edges of exactly three distinct semantic classes (sequence, impasse-spawn, resolution), identifiable via an edge.dat…", + "body": "Layout output contains edges of exactly three distinct semantic classes (sequence, impasse-spawn, resolution), identifiable via an edge.data.kind discriminator or edge.type. No edge in the output is generated by iterating ArtifactFile.graph.edges (EdgeRecord rows). Verify by (a) inspecting layout output for a fixture and asserting every edge has kind ∈ {sequence, impasse-spawn, resolution}, and (b) a unit test that inserts arbitrary EdgeRecord rows into the artifact and asserts the macro layout edge count is unchanged.", + "basis": "explicit", + "source": "derived [CR52]", + "detail": null + }, + { + "local_id": 163, + "plane": "intent", + "kind": "context", + "title": "The mandate for high information density combined with the CRT aesthetic and prohibition on generic UI patterns creates tension between den…", + "body": "The mandate for high information density combined with the CRT aesthetic and prohibition on generic UI patterns creates tension between dense data display and visual readability/non-overwhelm.", + "basis": "accepted_review_set", + "source": "derived-risk-or-question | derived-inferred [RK5]", + "detail": null + }, + { + "local_id": 164, + "plane": "intent", + "kind": "criterion", + "title": "An ImpasseNode renders with a clearly non-rectangular silhouette: either via SVG path/polygon or CSS clip-path/transform producing a diamon…", + "body": "An ImpasseNode renders with a clearly non-rectangular silhouette: either via SVG path/polygon or CSS clip-path/transform producing a diamond/lozenge shape. Its DOM contains the hub's displayId text and uses --color-phosphor-red as a glyph or border token. Verify with an RTL test asserting the presence of an SVG diamond polygon or a clip-path style, plus the text and color assertions.", + "basis": "explicit", + "source": "derived [CR49]", + "detail": null + }, + { + "local_id": 165, + "plane": "intent", + "kind": "criterion", + "title": "Given a fixture artifact representing a full onion-peel cycle (initial derivation, an impasse, a rederive child frame, fan-out runs, fan-in…", + "body": "Given a fixture artifact representing a full onion-peel cycle (initial derivation, an impasse, a rederive child frame, fan-out runs, fan-in, reconciliation, resolution), the rendered macro view contains: at least one PhaseGroupNode for the parent frame, an ImpasseNode at the lane boundary, at least one PhaseGroupNode for the child frame in a deeper lane, RunNode(s) and FanInNode and ReconciliationNode inside the child phase group, an impasse-spawn edge, and a resolution edge back to the impasse. Verify with an end-to-end RTL test on the full-cycle fixture.", + "basis": "explicit", + "source": "derived [CR70]", + "detail": null + }, + { + "local_id": 166, + "plane": "intent", + "kind": "requirement", + "title": "Edges whose both endpoints lie inside a collapsed PhaseGroupNode shall not be rendered while that group is collapsed.", + "body": "Edges whose both endpoints lie inside a collapsed PhaseGroupNode shall not be rendered while that group is collapsed. Edges with exactly one endpoint inside a collapsed group shall reattach to the collapsed pill rather than being hidden.", + "basis": "explicit", + "source": "derived [R56]", + "detail": null + }, + { + "local_id": 167, + "plane": "intent", + "kind": "requirement", + "title": "story-ir.ts and layout.ts shall export pure functions: given identical inputs they shall produce structurally equal outputs and shall not m…", + "body": "story-ir.ts and layout.ts shall export pure functions: given identical inputs they shall produce structurally equal outputs and shall not mutate their input data, perform I/O, or read from external state (DOM, time, stores).", + "basis": "explicit", + "source": "derived [R61]", + "detail": null + }, + { + "local_id": 168, + "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": 169, + "plane": "intent", + "kind": "requirement", + "title": "The MacroView shall load ArtifactFile data exactly once on component mount, build the Story IR, run layout, and freeze the resulting React…", + "body": "The MacroView shall load ArtifactFile data exactly once on component mount, build the Story IR, run layout, and freeze the resulting React Flow nodes and edges into component state. It shall not subscribe to or react to subsequent artifact changes.", + "basis": "explicit", + "source": "derived [R5]", + "detail": null + }, + { + "local_id": 170, + "plane": "intent", + "kind": "criterion", + "title": "A DerivationRunNode with status='failed' has computed border-color resolving to --color-phosphor-red and a visibly dimmed interior (e.g., r…", + "body": "A DerivationRunNode with status='failed' has computed border-color resolving to --color-phosphor-red and a visibly dimmed interior (e.g., reduced opacity on the body OR a dark overlay; quantified as effective body luminance ≤ 70% of completed baseline). Verify with an RTL + computed-style test asserting border color match and an opacity/filter property indicating dimming.", + "basis": "explicit", + "source": "derived [CR44]", + "detail": null + }, + { + "local_id": 171, + "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": 172, + "plane": "intent", + "kind": "criterion", + "title": "When MacroView is mounted with a mocked artifact loader, the loader is invoked exactly once.", + "body": "When MacroView is mounted with a mocked artifact loader, the loader is invoked exactly once. When the underlying artifact source emits subsequent change notifications (mocked), the loader is NOT re-invoked and the rendered RF nodes/edges remain referentially stable. Verify with a Vitest test using a spy on the loader and a mock store that emits changes after mount.", + "basis": "explicit", + "source": "derived [CR6]", + "detail": null + }, + { + "local_id": 173, + "plane": "intent", + "kind": "criterion", + "title": "A DerivationRunNode with status='completed' has no border color matching --color-phosphor-red and no opacity/dimming reduction relative to…", + "body": "A DerivationRunNode with status='completed' has no border color matching --color-phosphor-red and no opacity/dimming reduction relative to the base node treatment. Verify with an RTL + computed-style test on a completed-status fixture.", + "basis": "explicit", + "source": "derived [CR43]", + "detail": null + }, + { + "local_id": 174, + "plane": "intent", + "kind": "criterion", + "title": "A static-analysis scan of all files under src/components/macro/**/*.{ts,tsx,css} finds zero literal color values matching /#[0-9a-fA-F]{3,8…", + "body": "A static-analysis scan of all files under src/components/macro/**/*.{ts,tsx,css} finds zero literal color values matching /#[0-9a-fA-F]{3,8}/, /\\brgb\\(/, /\\brgba\\(/, /\\bhsl\\(/, /\\bhsla\\(/, or /\\boklch\\(/ outside of var() references. All colors are referenced via var(--color-*) tokens defined in theme.css. Verify with a regex-based unit test scanning the directory.", + "basis": "explicit", + "source": "derived [CR35]", + "detail": null + }, + { + "local_id": 175, + "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": 176, + "plane": "intent", + "kind": "requirement", + "title": "The macro view implementation shall not modify the existing Sigma.js-based micro view, the routing layer, or unrelated parts of the Spec Ex…", + "body": "The macro view implementation shall not modify the existing Sigma.js-based micro view, the routing layer, or unrelated parts of the Spec Explorer UI. Changes are scoped to src/components/macro/, the existing src/components/MacroView.tsx re-export, and any minimal extensions to src/components/DetailPanel.tsx required to render macro record kinds.", + "basis": "explicit", + "source": "derived [R59]", + "detail": null + }, + { + "local_id": 177, + "plane": "intent", + "kind": "requirement", + "title": "DerivationRunNode, FanInNode, ReconciliationNode, and PerspectiveNode instances belonging to a phase shall be rendered as React Flow childr…", + "body": "DerivationRunNode, FanInNode, ReconciliationNode, and PerspectiveNode instances belonging to a phase shall be rendered as React Flow children of their PhaseGroupNode using parentId and extent='parent'. The PhaseGroupNode shall be a React Flow group/parent node (type='group' or equivalent).", + "basis": "explicit", + "source": "derived [R12]", + "detail": null + }, + { + "local_id": 178, + "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": 179, + "plane": "intent", + "kind": "requirement", + "title": "Clicking on a non-faded, non-phantom macro node shall dispatch a select action carrying the underlying IR record (frame, derivation run, fa…", + "body": "Clicking on a non-faded, non-phantom macro node shall dispatch a select action carrying the underlying IR record (frame, derivation run, fan-in, reconciliation, impasse hub, or perspective hub) into the existing global selection store consumed by DetailPanel.tsx.", + "basis": "explicit", + "source": "derived [R29]", + "detail": null + }, + { + "local_id": 180, + "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": 181, + "plane": "oracle", + "kind": "evidence", + "title": "The data structures for the macro view are defined in /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation-ui/s…", + "body": "The data structures for the macro view are defined in /Users/bmahmoud/projects/development/kael/packages/experimental/spec-elicitation-ui/src/types/artifact.ts.", + "basis": "explicit", + "source": "stakeholder-observed [E1]", + "detail": null + }, + { + "local_id": 182, + "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": 183, + "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": 184, + "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": 185, + "plane": "intent", + "kind": "criterion", + "title": "For each phase group containing one or more DerivationRunNodes, a FanInNode, and a ReconciliationNode, layout emits sequence edges from eac…", + "body": "For each phase group containing one or more DerivationRunNodes, a FanInNode, and a ReconciliationNode, layout emits sequence edges from each RunNode → FanInNode and FanInNode → ReconciliationNode. Each such edge has computed stroke color resolving to --color-phosphor-amber, has a markerEnd arrow, and has animated !== true. Verify with a unit-test fixture and a render assertion on edge count, source/target ids, and computed style.", + "basis": "explicit", + "source": "derived [CR53]", + "detail": null + }, + { + "local_id": 186, + "plane": "intent", + "kind": "criterion", + "title": "Given two sibling phase groups A and B at the same depth where B is below A in the y axis, after collapsing A, B's new position.y is strict…", + "body": "Given two sibling phase groups A and B at the same depth where B is below A in the y axis, after collapsing A, B's new position.y is strictly less than its previous position.y by approximately the height differential (expanded_height(A) - pill_height) ± a tolerance. No sibling node retains a position that would leave a vertical gap larger than expanded_height(A)/2 where A's expanded body used to be. Verify with a unit test on layout() comparing pre-collapse and post-collapse outputs.", + "basis": "explicit", + "source": "derived [CR27]", + "detail": null + }, + { + "local_id": 187, + "plane": "intent", + "kind": "criterion", + "title": "Given two depths D1 and D2 where the maximum content width across nodes at D1 is W1 and at D2 is W2, with W1 != W2, the lane widths assigne…", + "body": "Given two depths D1 and D2 where the maximum content width across nodes at D1 is W1 and at D2 is W2, with W1 != W2, the lane widths assigned by layout differ (laneWidth(D1) != laneWidth(D2)) and laneWidth(D_i) is a function of W_i (not a constant). Verify with a parameterized unit test asserting the lane-width function is non-constant across two fixtures with deliberately differing content widths.", + "basis": "explicit", + "source": "derived [CR19]", + "detail": null + }, + { + "local_id": 188, + "plane": "intent", + "kind": "criterion", + "title": "Given a fixture where impasse I in frame F0 triggers child frame F1 (parent F0, depth 1), the layout positions ImpasseNode I at the boundar…", + "body": "Given a fixture where impasse I in frame F0 triggers child frame F1 (parent F0, depth 1), the layout positions ImpasseNode I at the boundary between F0's lane (depth 0) and F1's lane (depth 1) such that it is horizontally between (or aligned with the start of) the two lanes. Verify with a unit test on layout asserting I.position.x lies between the rightmost x of F0's nodes and the leftmost x of F1's nodes (inclusive).", + "basis": "explicit", + "source": "derived [CR65]", + "detail": null + }, + { + "local_id": 189, + "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": 190, + "plane": "intent", + "kind": "requirement", + "title": "Collapse/expand state for phase groups shall be held in a single useState> (or equivalent set keyed by phase-group identity) a…", + "body": "Collapse/expand state for phase groups shall be held in a single useState> (or equivalent set keyed by phase-group identity) at the MacroView root component. PhaseGroupNode renderers shall not own their own collapse state.", + "basis": "explicit", + "source": "derived [R22]", + "detail": null + }, + { + "local_id": 191, + "plane": "intent", + "kind": "criterion", + "title": "ReconciliationNode renders with computed border-color: outcome='accepted' → the parent phase's --color-phase-* token; outcome='retry' → --c…", + "body": "ReconciliationNode renders with computed border-color: outcome='accepted' → the parent phase's --color-phase-* token; outcome='retry' → --color-phosphor-amber; outcome='recurse' → --color-phosphor-cyan; outcome='bail' → --color-phosphor-red AND a dimmed interior treatment. Header contains a textual chip whose text equals the outcome name. Verify with parameterized RTL tests across all four outcomes.", + "basis": "explicit", + "source": "derived [CR47]", + "detail": null + }, + { + "local_id": 192, + "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": 193, + "plane": "intent", + "kind": "criterion", + "title": "A heuristic content-completeness test asserts that every visible (non-collapsed, non-faded) macro node renders text/visual elements coverin…", + "body": "A heuristic content-completeness test asserts that every visible (non-collapsed, non-faded) macro node renders text/visual elements covering at minimum: a unique ID (frame displayId, run index, hub displayId, etc.), a count or status indicator (run counts, outcome glyph, fan-in row count, or impassesFound), and (where applicable) a mode/outcome chip. Verify with an RTL-driven content audit: for each node-type fixture, assert presence of (a) ID text, (b) numeric or glyph indicator, (c) status/mode chip text where applicable.", + "basis": "explicit", + "source": "derived [CR63]", + "detail": null + }, + { + "local_id": 194, + "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": 195, + "plane": "intent", + "kind": "requirement", + "title": "Every visible (non-collapsed, non-faded) macro node shall surface enough content (IDs, counts, outcome glyph, mode chip) to identify what h…", + "body": "Every visible (non-collapsed, non-faded) macro node shall surface enough content (IDs, counts, outcome glyph, mode chip) to identify what happened at that derivation step without requiring the user to open the detail panel.", + "basis": "explicit", + "source": "derived [R63]", + "detail": null + }, + { + "local_id": 196, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall render exactly one DerivationRunNode per DerivationRunRecord, one FanInNode per FanInRecord, and one ReconciliationNod…", + "body": "The macro view shall render exactly one DerivationRunNode per DerivationRunRecord, one FanInNode per FanInRecord, and one ReconciliationNode per ReconciliationRecord present in ArtifactFile.graph.", + "basis": "explicit", + "source": "derived [R13]", + "detail": null + }, + { + "local_id": 197, + "plane": "intent", + "kind": "criterion", + "title": "Given a fixture phase group with no PerspectiveNode whose perspectiveStatus is 'selected', buildStoryIR emits exactly one PhantomNode child…", + "body": "Given a fixture phase group with no PerspectiveNode whose perspectiveStatus is 'selected', buildStoryIR emits exactly one PhantomNode child for that phase group whose id is synthesized (not present in ArtifactFile) and whose label contains the phrase 'no perspective taken' (case-insensitive). Conversely, for a phase group containing a selected perspective, no PhantomNode is emitted. Verify with two unit-test fixtures.", + "basis": "explicit", + "source": "derived [CR16]", + "detail": null + }, + { + "local_id": 198, + "plane": "intent", + "kind": "criterion", + "title": "When a PhaseGroupNode is collapsed, the rendered pill DOM contains: (1) an element styled with background-color or border-color resolving t…", + "body": "When a PhaseGroupNode is collapsed, the rendered pill DOM contains: (1) an element styled with background-color or border-color resolving to the corresponding --color-phase-* token, (2) the frame's displayId text, (3) text matching the pattern /\\d+\\s+RUNS?/, and (4) one of the glyphs ✓, ↺, ↪, or ✗ corresponding to the frame's terminal ReconciliationRecord.outcome (accepted/retry/recurse/bail respectively). Clicking the pill toggles expansion AND dispatches a select action. Verify with a parameterized RTL test covering all four outcomes.", + "basis": "explicit", + "source": "derived [CR26]", + "detail": null + }, + { + "local_id": 199, + "plane": "intent", + "kind": "criterion", + "title": "Inspecting the MacroView component source (or its rendered React tree) shows exactly one useState/useReducer call holding a Set (or Set-equ…", + "body": "Inspecting the MacroView component source (or its rendered React tree) shows exactly one useState/useReducer call holding a Set (or Set-equivalent) of FrameId values for collapsed groups, located in the MacroView root component. PhaseGroupNode component source contains no useState/useReducer holding collapse state. Verify with a static-analysis test (AST inspection of the source files) and/or a runtime test using React DevTools-equivalent introspection.", + "basis": "explicit", + "source": "derived [CR22]", + "detail": null + }, + { + "local_id": 200, + "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": 201, + "plane": "intent", + "kind": "criterion", + "title": "Calling buildStoryIR(artifact) twice with structurally-equal artifact inputs produces deeply-equal outputs.", + "body": "Calling buildStoryIR(artifact) twice with structurally-equal artifact inputs produces deeply-equal outputs. Calling layout(ir, set) twice with structurally-equal inputs produces deeply-equal outputs. Neither function mutates its input (pre/post deep-equal of inputs). Neither references Date.now, Math.random, document, window, or external store. Verify with property-based tests (fast-check) for determinism and idempotence, plus a static-analysis test for forbidden globals.", + "basis": "explicit", + "source": "derived [CR61]", + "detail": null + }, + { + "local_id": 202, + "plane": "intent", + "kind": "requirement", + "title": "An ImpasseNode whose hub is the trigger of a child frame whose terminal ReconciliationRecord.outcome is 'bail' shall be annotated with a 'D…", + "body": "An ImpasseNode whose hub is the trigger of a child frame whose terminal ReconciliationRecord.outcome is 'bail' shall be annotated with a 'DEAD-END' textual chip on the node, distinguishing it from open or resolved impasses.", + "basis": "explicit", + "source": "derived [R49]", + "detail": null + }, + { + "local_id": 203, + "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": 204, + "plane": "intent", + "kind": "criterion", + "title": "story-ir.ts exports a buildStoryIR(artifact) function whose return type is a typed StoryIR (not RFNode[]); layout.ts exports a layout(ir, c…", + "body": "story-ir.ts exports a buildStoryIR(artifact) function whose return type is a typed StoryIR (not RFNode[]); layout.ts exports a layout(ir, collapsedSet) function whose return type contains RFNode[] and RFEdge[] with absolute positions. Verify by a TypeScript type-level test (tsd or expectTypeOf) that the IR builder's output has no React Flow position/parentId fields and that the layout output's nodes array contains objects with {id, type, position, parentId?}.", + "basis": "explicit", + "source": "derived [CR5]", + "detail": null + }, + { + "local_id": 205, + "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": 206, + "plane": "intent", + "kind": "criterion", + "title": "For each HubNode with hubType='impasse' that participates in the derivation narrative (i.e., is referenced via FrameRecord.triggerImpasseId…", + "body": "For each HubNode with hubType='impasse' that participates in the derivation narrative (i.e., is referenced via FrameRecord.triggerImpasseIds or ReconciliationRecord.resolvedImpasseIds/triggerImpasseIds/unresolvedImpasseIds), the IR contains exactly one ImpasseNode whose id maps to that hub. Likewise for each participating HubNode with hubType='perspective'. Verify with a unit test on buildStoryIR using a fixture covering all three reference paths.", + "basis": "explicit", + "source": "derived [CR15]", + "detail": null + }, + { + "local_id": 207, + "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": 208, + "plane": "intent", + "kind": "requirement", + "title": "All macro view edges shall render without animation by default; no edge shall use React Flow's animated property by default.", + "body": "All macro view edges shall render without animation by default; no edge shall use React Flow's animated property by default.", + "basis": "explicit", + "source": "derived [R55]", + "detail": null + }, + { + "local_id": 209, + "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": 210, + "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": 211, + "plane": "intent", + "kind": "criterion", + "title": "Clicking on a PerspectiveNode whose perspectiveStatus is 'rejected' or 'open' (i.e., faded) does NOT dispatch any select action.", + "body": "Clicking on a PerspectiveNode whose perspectiveStatus is 'rejected' or 'open' (i.e., faded) does NOT dispatch any select action. Its DOM exposes no interactive affordance. Verify with an RTL test using fixtures for both selected and faded perspective nodes; click each; assert select dispatch only for the selected one.", + "basis": "explicit", + "source": "derived [CR33]", + "detail": null + }, + { + "local_id": 212, + "plane": "intent", + "kind": "criterion", + "title": "The full project builds with `vite build` (or its equivalent npm/deno script) without errors after adding the macro view.", + "body": "The full project builds with `vite build` (or its equivalent npm/deno script) without errors after adding the macro view. No new bundler or runtime is added (no Webpack, Parcel, esbuild standalone, etc., introduced). package.json/deno.json shows no new CSS framework dependency competing with Tailwind. Verify with a CI build step plus a dependency-list check.", + "basis": "explicit", + "source": "derived [CR60]", + "detail": null + }, + { + "local_id": 213, + "plane": "intent", + "kind": "requirement", + "title": "The macro view shall render one ImpasseNode per HubNode whose hubType is 'impasse', and one PerspectiveNode per HubNode whose hubType is 'p…", + "body": "The macro view shall render one ImpasseNode per HubNode whose hubType is 'impasse', and one PerspectiveNode per HubNode whose hubType is 'perspective', subject to those hubs participating in the derivation narrative captured by the IR.", + "basis": "explicit", + "source": "derived [R14]", + "detail": null + }, + { + "local_id": 214, + "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": 215, + "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 + }, + { + "local_id": 216, + "plane": "intent", + "kind": "criterion", + "title": "On every fresh mount of MacroView with any artifact fixture, immediately after first render, no PhaseGroupNode is rendered as a collapsed p…", + "body": "On every fresh mount of MacroView with any artifact fixture, immediately after first render, no PhaseGroupNode is rendered as a collapsed pill: every phase group renders in its expanded form. Verify with a render test that mounts MacroView and asserts (a) the collapsed-set state is empty and (b) no element with the macro-collapsed-pill data attribute is in the DOM.", + "basis": "explicit", + "source": "derived [CR23]", + "detail": null + }, + { + "local_id": 217, + "plane": "intent", + "kind": "decision", + "title": "Hoist a single Set of collapsed IDs to MacroView root, in-memory only.", + "body": "Hoist a single Set of collapsed IDs to MacroView root, in-memory only.", + "basis": "explicit", + "source": "[DEC6]", + "detail": { + "chosen_option": "Collapse/expand state lives in a single useState> at the MacroView root, holding the IDs of currently-collapsed phase groups (or frames). The set starts empty on mount (everything expanded per C9) and is never written to localStorage, sessionStorage, URL, or any persistence layer (per C8). Toggle handlers are passed down via React context to PhaseGroupNode renderers, which swap to a compact pill renderer when their ID is in the set. After a toggle, the layout function re-runs synchronously over the IR + collapsed-set to produce new node positions, and React Flow's animated transitions (default fitView=false, but applyNodeChanges with smooth-tweened positions) handle reflow per X22.", + "rejected": [ + "Alternative: persist collapse state across reloads (localStorage). Rejected by C8/C9.", + "Alternative: each PhaseGroupNode owns its own useState for collapsed/expanded. Local but means parent layout cannot recompute on toggle without lifting state anyway." + ], + "rationale": "Collapse changes the global layout (sibling shifts, X22), so the set must be visible to the layout function; per-node local state forces a redundant lift. C8/C9 forbid persistence, ruling out localStorage. A single Set keeps the toggle O(1), is trivially serializable for unit tests, and makes the snapshot-load+expanded-default invariant a one-liner (initial state = empty set)." + } + }, + { + "local_id": 218, + "plane": "intent", + "kind": "decision", + "title": "Render only the three workflow edge classes (sequence, impasse-spawn, resolution), synthesized from frame/run/fan-in/reconciliation records…", + "body": "Render only the three workflow edge classes (sequence, impasse-spawn, resolution), synthesized from frame/run/fan-in/reconciliation records, not from EdgeRecord rows.", + "basis": "explicit", + "source": "[DEC9]", + "detail": { + "chosen_option": "Edges in the macro view encode three workflow relationships, each rendered as a typed React Flow edge: (1) sequence edges — thin amber lines connecting RunNode → FanInNode → ReconciliationNode within a phase group, expressing fan-out→fan-in→reconcile flow (T4/T5/T6); (2) impasse-spawn edges — red dashed lines from a ReconciliationNode (or a PhaseGroupNode whose phase produced impasses) outward to the ImpasseNode that opened a new lane, expressing that the impasse caused a child frame (X16, FrameRecord.triggerImpasseIds); (3) resolution edges — phase-colored solid lines from a child frame's terminal ReconciliationNode (or activated nodes) back to the impasse it resolved, drawn with a return-leftward routing convention (T6.resolvedImpasseIds). Edges between sibling phase groups inside a frame follow PHASE_ORDER. All edges use markerEnd arrows, no animation by default. Edges within a collapsed group are hidden along with the group's children; the group's connecting external edges remain attached at the pill's perimeter.", + "rejected": [ + "Alternative: derive every edge mechanically from EdgeRecord rows in the artifact (informed_by, produced, considered, etc.). This pushes graph-content edges (designed for the micro view) into the macro view and would generate hundreds of edges, defeating the macro view's narrative purpose.", + "Alternative: render no edges at all and rely on spatial proximity / containment to imply flow. Cleaner visually but loses the narrative arrow of impasse → child frame → resolution that the macro view exists to tell." + ], + "rationale": "The macro view narrates the derivation process (G1, X3); content-level edges belong to the micro view (X2). Three semantically named edge classes give the narrative structure (fan-out→fan-in→reconcile, impasse opens a lane, resolution closes it back) while keeping the rendered edge count proportional to the ~20–40 nodes. Implicit-only edges leave the resolution arc invisible. Each edge class uses one already-justified color (amber for trunk flow, red for impasse, phase color for resolution), respecting C7." + } + }, + { + "local_id": 219, + "plane": "intent", + "kind": "decision", + "title": "Animated phosphor-arrive loop + cyan 'RUNNING' chip + scanline sweep on running runs.", + "body": "Animated phosphor-arrive loop + cyan 'RUNNING' chip + scanline sweep on running runs.", + "basis": "explicit", + "source": "[DEC10]", + "detail": { + "chosen_option": "When a DerivationRunRecord.status is 'running' (rare per X26), the RunNode renders with: (a) the existing phosphor-arrive keyframe (already in theme.css) looping at slow tempo on the node body, (b) a 'RUNNING' chip in --color-phosphor-cyan in the header (cyan is unused for outcome semantics elsewhere, so it carries no conflicting meaning), and (c) a thin animated scanline sweep across the node interior. The node remains clickable and shows the same content fields as a completed run.", + "rejected": [ + "Alternative: a static cyan 'RUNNING' chip with no animation. Calmer but defeats X26's 'highlighted somehow' intent.", + "Alternative: amber pulsing border. Rejected because amber already encodes nudging (X25) and reconciliation 'retry' outcome (X28); reusing it for running creates ambiguity, violating C7's 'every color earns its place'." + ], + "rationale": "X26 says running is unlikely but must be highlighted. Cyan is the one phosphor token not yet load-bearing in macro semantics (red=failure/bail, amber=nudging/retry, green=merged/success, purple=defining_done phase, phase colors=phase identity), so it cleanly marks an in-flight state without colliding with C7. Animation distinguishes running from any static state at a glance. Reusing the existing phosphor-arrive keyframe keeps the addition cheap and within the established CRT motion vocabulary." + } + }, + { + "local_id": 220, + "plane": "intent", + "kind": "decision", + "title": "Use a dedicated Story IR layer between artifact data and rendering.", + "body": "Use a dedicated Story IR layer between artifact data and rendering.", + "basis": "explicit", + "source": "[DEC1]", + "detail": { + "chosen_option": "Architect MacroView as a three-stage pure pipeline: (1) Story IR builder that consumes ArtifactFile.graph and produces a normalized derivation tree (FrameNode root → PhaseNode children → RunNode/FanInNode/ReconciliationNode/PerspectiveNode/ImpasseNode descendants, with parentFrameId chains expanded into nested impasse branches), (2) Layout engine that walks the IR and computes absolute (x,y) positions, lane widths, and parent/child grouping, (3) React Flow renderer that maps IR nodes to custom node types and IR edges to typed RF edges. The IR is the only contract layout and rendering depend on; data shape changes localize to stage 1.", + "rejected": [ + "Alternative: skip the Story IR and map ArtifactFile records directly to React Flow nodes inside one component, with layout calculation interleaved with rendering.", + "Alternative: model the derivation history as a generic graphlib graph and feed it through a layered layout, then translate to React Flow at the end." + ], + "rationale": "The macro view's spatial grammar (onion-peel breadth, phase containment, fan-out/fan-in nesting, perspective fade) is highly domain-specific and unstable while the design is being iterated. A typed IR isolates the domain mapping from layout math from React Flow specifics, letting each stage be unit-testable and letting the manual layout (mandated by C3) operate on a tree shape that already encodes parent/child semantics rather than re-deriving them. Direct RF mapping conflates concerns and makes the collapse/reflow logic (X22) harder. A generic graphlib representation loses the typed semantics the custom node renderers need." + } + }, + { + "local_id": 221, + "plane": "intent", + "kind": "decision", + "title": "Encode information across orthogonal visual channels (border style, border color, fill, header chips, opacity, shape) drawn from existing p…", + "body": "Encode information across orthogonal visual channels (border style, border color, fill, header chips, opacity, shape) drawn from existing phosphor tokens.", + "basis": "explicit", + "source": "[DEC8]", + "detail": { + "chosen_option": "Each node type expresses its semantic role through a fixed visual vocabulary built from theme.css tokens: (a) PhaseGroupNode — 1px border in the phase color (--color-phase-*), warm dark surface-1 fill, scanline overlay, header line 'PHASE / FRAME-ID / mode' in --color-text-secondary; mode differentiation per X20 done by border style (initial=solid, rederive=double, grounding_enrichment=dashed) plus a small mode chip; nudgingActive shown as a 'NUDGING' chip in --color-phosphor-amber inside the header (X25). (b) DerivationRunNode — numbered tile 'RUN #n' with input/output count badges and impassesFound count; status='completed' is base, status='failed' uses --color-phosphor-red border and dimmed interior (X27), status='running' adds an animated phosphor-arrive pulse (X26). (c) FanInNode — stacked rows, one per FanInGrouping, each row prefixed by a 4px left border in green/amber/red per resolution (X32); row text shows groupKey and node count. (d) ReconciliationNode — outcome encoded as full-node border color (accepted=phase color, retry=amber, recurse=cyan/blue, bail=red+dim) per X28, plus an outcome chip in the header; materialProgress=true shown as a small ✓ chip beside the outcome (X35). (e) ImpasseNode — diamond/lozenge shape with red glyph, displayId visible; if linked to a bail reconciliation (X36), it is annotated 'DEAD-END' to disambiguate from open impasses (RK2 mitigation). (f) PerspectiveNode — branching tile; selected branch full opacity, rejected branches at ~30% opacity (X23); non-interactive when faded (X24). (g) PhantomNode — dashed-outline ghost tile, no fill, label 'PHANTOM — no perspective taken', non-interactive (X24).", + "rejected": [ + "Alternative: lean heavily on icons (status icons, mode icons, outcome icons) instead of typographic chips and border treatments. Punchier visually but less information-dense per pixel and breaks the typographic CRT aesthetic.", + "Alternative: encode all status/mode/outcome differences via fill color alone, leaving borders neutral. Easier but loses the orthogonal channels (border = outcome, fill = mode, chip = nudging) that let several attributes coexist on one node." + ], + "rationale": "G3 demands at-a-glance comprehension and G4 demands that each node communicates its specific outcome; a single channel cannot carry mode + status + outcome + nudging + materialProgress simultaneously. Orthogonal channels honor C7 (every color earns meaning: border=outcome, phase color=phase, red=failure/bail, amber=warning states). Iconographic styling fights the JetBrains Mono / typographic CRT aesthetic (X11, X37). Reusing the seven oklch tokens already defined in theme.css avoids palette inflation and keeps C13 satisfied. The intentional collision between bail-outcome and failed-run treatment (X29) is preserved; RK2 is mitigated by adding a 'DEAD-END' textual chip on bail-linked impasses rather than diverging the color treatment." + } + }, + { + "local_id": 222, + "plane": "intent", + "kind": "decision", + "title": "Decompose into a src/components/macro/ folder with one file per pipeline stage and one file per node type.", + "body": "Decompose into a src/components/macro/ folder with one file per pipeline stage and one file per node type.", + "basis": "explicit", + "source": "[DEC13]", + "detail": { + "chosen_option": "Module structure under src/components/macro/: index.ts (re-exports MacroView), MacroView.tsx (top-level: data load, state, ReactFlowProvider, Canvas), story-ir.ts (ArtifactFile → StoryIR builder, pure), layout.ts (StoryIR + collapsedSet → RFNode[]/RFEdge[], pure), nodes/ (PhaseGroupNode.tsx, DerivationRunNode.tsx, FanInNode.tsx, ReconciliationNode.tsx, ImpasseNode.tsx, PerspectiveNode.tsx, PhantomNode.tsx — one component per type), edges/ (custom edge components if needed), and macro.css or co-located CSS modules using only theme.css tokens. Existing src/components/MacroView.tsx becomes a thin re-export of the new module to preserve the current import path.", + "rejected": [ + "Alternative: keep everything inside the existing single MacroView.tsx file. Faster to start but conflicts with the pipeline boundaries (dec-pipeline) and clusters seven node renderers into one file." + ], + "rationale": "The pipeline decision (dec-pipeline) and the seven-node taxonomy (dec-node-taxonomy) both imply natural file boundaries. Co-locating the macro folder under components keeps the project's existing layout convention (sibling to DetailPanel.tsx). Re-exporting through the original MacroView.tsx path means routes/explore.tsx keeps working unchanged. C5 explicitly scopes this work to the macro view, so a dedicated folder helps reviewers see the scope boundary." + } + }, + { + "local_id": 223, + "plane": "intent", + "kind": "decision", + "title": "Adopt seven typed React Flow node components, no separate trunk type.", + "body": "Adopt seven typed React Flow node components, no separate trunk type.", + "basis": "explicit", + "source": "[DEC4]", + "detail": { + "chosen_option": "Define exactly seven custom React Flow node types matching the data shapes: PhaseGroupNode (RF group/parent node, one per FrameRecord+Phase pair, mode-tinted), DerivationRunNode (one per DerivationRunRecord, child of PhaseGroupNode), FanInNode (one per FanInRecord, child of PhaseGroupNode, contains color-coded grouping rows), ReconciliationNode (one per ReconciliationRecord, child of PhaseGroupNode), ImpasseNode (one per HubNode with hubType='impasse', positioned at the boundary opening a new lane), PerspectiveNode (one per HubNode with hubType='perspective'), and PhantomNode (synthesized when a phase group ends without a perspective selection per T10). The trunk is not a node type; it emerges from PhaseGroupNodes laid out at depth 0 (per X19).", + "rejected": [ + "Alternative: introduce an explicit TrunkNode type for depth-0 phase groups, separate from nested phase groups.", + "Alternative: one polymorphic 'MacroNode' component that switches on a discriminator prop. Reduces the React Flow nodeTypes registry but conflates radically different visual treatments and makes per-type styling and testing harder." + ], + "rationale": "X13 enumerates the desired semantic node types; one React component per type aligns the type system with the visual grammar and gives each node its own focused render path (G3/G4 require visual distinctness at a glance). A polymorphic component would push that complexity into a single switch and erode TypeScript support. X19 explicitly says trunk is not a separate type; reusing PhaseGroupNode at depth 0 honors that and keeps mode tinting (X20) as the only differentiator. Dead-end impasse is handled as a visual variant of ImpasseNode driven by the linked ReconciliationRecord.outcome='bail' (X36), not as an eighth type, because behaviorally it is still an impasse." + } + }, + { + "local_id": 224, + "plane": "intent", + "kind": "decision", + "title": "Show a snapshot timestamp + reload affordance as a fixed corner overlay.", + "body": "Show a snapshot timestamp + reload affordance as a fixed corner overlay.", + "basis": "explicit", + "source": "[DEC11]", + "detail": { + "chosen_option": "Render a small permanently-visible 'SNAPSHOT @ ' badge in the macro view's top-left corner using --color-text-tertiary, with a 'RELOAD' button next to it. Clicking RELOAD re-runs the pipeline. The banner sits above the React Flow canvas as a fixed overlay; it does not participate in pan/zoom.", + "rejected": [ + "Alternative: no banner, rely on user knowledge that the view is snapshot-only. Leaves RK3 (stale data) fully unmitigated." + ], + "rationale": "RK3 (users may view stale history) is a real and silent failure mode. Surfacing the snapshot time directly in the view turns it from invisible to glanceable, while a Reload button makes the manual-refresh expectation actionable without violating C11/C12 (the data contract is still snapshot-on-load; Reload is an explicit re-mount). The treatment is small and uses an existing token (text-tertiary), so it doesn't compete with the derivation graph for attention." + } + }, + { + "local_id": 225, + "plane": "intent", + "kind": "decision", + "title": "Snapshot the artifact on mount; require manual reload.", + "body": "Snapshot the artifact on mount; require manual reload.", + "basis": "explicit", + "source": "[DEC2]", + "detail": { + "chosen_option": "Load the artifact exactly once on component mount via a useEffect/useMemo against the artifact source (file fetch or store selector), build the Story IR, run layout, and freeze the resulting React Flow nodes/edges arrays into useState. No subscriptions, no live updates. Provide a manual 'Reload' affordance that re-runs the pipeline.", + "rejected": [ + "Alternative: subscribe to the artifact store and recompute the IR/layout on every change." + ], + "rationale": "C11/C12 explicitly mandate snapshot-only behavior. Live subscription would invalidate the layout mid-interaction and conflict with the ephemeral collapse state (C8), causing layouts to thrash. A visible 'Reload' affordance partially mitigates RK3 (stale data) without breaking the snapshot contract." + } + }, + { + "local_id": 226, + "plane": "intent", + "kind": "decision", + "title": "Collapse to a stat-bearing pill, not an icon.", + "body": "Collapse to a stat-bearing pill, not an icon.", + "basis": "explicit", + "source": "[DEC12]", + "detail": { + "chosen_option": "When a PhaseGroupNode is in the collapsed set, its renderer swaps to a compact pill ~120px×28px showing: phase color dot + frame displayId + 'n RUNS' + outcome glyph (✓ accepted / ↺ retry / ↪ recurse / ✗ bail) derived from the frame's terminal reconciliation. The pill remains clickable to expand and to open the detail panel. External edges re-attach to the pill's center handles automatically because React Flow recomputes edge endpoints from node bounds.", + "rejected": [ + "Alternative: collapse to a tiny icon-only marker. Smaller but loses the at-a-glance run count + outcome that X21 calls out as 'key stats'." + ], + "rationale": "X21 explicitly says the collapsed form should show key stats (run count, outcome). G3 demands at-a-glance comprehension even in summary form. A pill carries the four bits of information (phase, frame ID, run count, outcome) using existing visual tokens; an icon-only marker forces the user to expand or open the detail panel just to recall what a frame contains, defeating the purpose of collapse-as-summary." + } + }, + { + "local_id": 227, + "plane": "intent", + "kind": "decision", + "title": "Reuse the existing DetailPanel component, extending it with branches for macro record kinds.", + "body": "Reuse the existing DetailPanel component, extending it with branches for macro record kinds.", + "basis": "explicit", + "source": "[DEC7]", + "detail": { + "chosen_option": "On node click, the MacroView reads the React Flow node's underlying IR record (frame, run, fan-in, reconciliation, or hub) and dispatches a 'select' action to the existing global selection store consumed by DetailPanel.tsx. The DetailPanel branches its rendering on record kind to display frame summary, run inputs/outputs, fan-in groupings, reconciliation deltas, etc. PhantomNodes and faded perspective branches do not dispatch any selection (per X24).", + "rejected": [ + "Alternative: build a separate macro-specific detail panel optimized for derivation records (frames/runs/reconciliations), since DetailPanel was originally built for graph nodes." + ], + "rationale": "X4 mandates reuse. The DetailPanel already handles selection plumbing, layout, and CRT styling; replicating that for the macro view would duplicate code and risk visual divergence. Adding record-kind branches inside DetailPanel keeps the selection contract single. Read-only enforcement (C10) is automatic because DetailPanel has no mutating actions wired in the macro path." + } + }, + { + "local_id": 228, + "plane": "intent", + "kind": "decision", + "title": "Use a custom recursive DFS lane layout with proportional lane widths.", + "body": "Use a custom recursive DFS lane layout with proportional lane widths.", + "basis": "explicit", + "source": "[DEC3]", + "detail": { + "chosen_option": "Manual layout algorithm: compute derivationDepth for each FrameRecord as the length of its parentFrameId chain; assign each frame to a horizontal lane indexed by depth (depth 0 = trunk at center, depth 1+ branches to the right). Within a lane, frames stack vertically with the most recent at the top (higher y ←→ more recent t+1). Each lane's width is computed as a function of the maximum content width across all nodes at that depth, not a fixed constant. Phase groups inside a frame stack vertically in PHASE_ORDER reverse (defining_done top, grounding bottom) so the eye reads downward through the derivation, while later frames sit above earlier ones at the lane level. The algorithm runs as a recursive DFS that returns subtree bounding boxes used to compute sibling offsets.", + "rejected": [ + "Alternative: use ELK.js layered layout with manual constraints to encode lanes. Cheaper to implement than full manual layout but less precise about onion-peel breadth and conflicts with C3.", + "Alternative: fixed lane width and fixed row height, regardless of content. Simple to implement but ignores X31's proportional-width preference and produces large dead space at shallow lanes." + ], + "rationale": "C3 forbids dagre and the equivalent argument applies to ELK: the spatial grammar (onion peel breadth = depth, verticality = time, impasses opening lanes per X16, perspective fan-out under phase groups) is too prescriptive for any general-purpose layout. A recursive DFS that returns subtree bounding boxes is a small amount of code (roughly 100–200 LOC) and gives full control. Fixed lanes were rejected because X31 specifies proportional sizing and because shallow trunks would otherwise look impoverished next to wide branches." + } + }, + { + "local_id": 229, + "plane": "intent", + "kind": "decision", + "title": "Use React Flow parent/child group nodes for phase containers.", + "body": "Use React Flow parent/child group nodes for phase containers.", + "basis": "explicit", + "source": "[DEC5]", + "detail": { + "chosen_option": "Use React Flow's parentId/extent='parent' mechanism so that DerivationRunNodes, FanInNodes, ReconciliationNodes, and PerspectiveNodes are children of a PhaseGroupNode (which is rendered as type='group'). Children use position relative to the parent's origin, and the layout algorithm emits absolute parent positions plus relative child offsets. This lets React Flow handle the visual containment, drag-bound clipping, and z-ordering for free, and it makes collapse-as-pill (X21) a matter of toggling the group's children to display:none and swapping its renderer to a compact pill.", + "rejected": [ + "Alternative: render the entire phase group's interior (runs, fan-in, reconciliation rows) as inner HTML inside a single React Flow node, without using RF parenting at all.", + "Alternative: render fan-out/fan-in/reconciliation as separate top-level nodes positioned to overlap a 'background' phase node; do not use React Flow parenting." + ], + "rationale": "X18 explicitly mandates RF group/parent nodes. Beyond compliance, parenting buys correct hit-testing, per-node click-to-detail (X33), and individual child animation when the group reflows on collapse (X22). HTML-only nesting forfeits the ability to attach edges from a fan-in row to a child reconciliation node and breaks the click-target model. Manual overlap is fragile and reorders interactively." + } + }, + { + "local_id": 230, + "plane": "intent", + "kind": "context", + "title": "The combination of mandatory snapshot-only data loading, the resulting risk of users viewing stale derivation history, and the requirement…", + "body": "The combination of mandatory snapshot-only data loading, the resulting risk of users viewing stale derivation history, and the requirement to keep the data contract unchanged jointly force a visible-snapshot-time + reload-button overlay (rather than no banner, or live-subscription).\n\n## Rationale\n\nC11/C12 mandate snapshot-only behavior; RK3 identifies stale-data viewing as a real silent failure; DEC2 forbids live subscriptions; DEC11 chooses a banner+reload affordance as the mitigation. Together these jointly require both (a) a visible snapshot timestamp and (b) an explicit reload control to be present in the UI — neither premise alone is sufficient.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J2]", + "detail": null + }, + { + "local_id": 231, + "plane": "intent", + "kind": "context", + "title": "Three independent constraints jointly force collapse state to be a single hoisted in-memory Set with no persistence and an empty initial va…", + "body": "Three independent constraints jointly force collapse state to be a single hoisted in-memory Set with no persistence and an empty initial value: (1) the requirement that collapse triggers global sibling reflow, (2) the prohibition on persistence, and (3) the mandate that every page load starts fully expanded.\n\n## Rationale\n\nX22 requires sibling reflow on collapse, which the layout function needs visibility into the full collapsed set to compute — forcing the state to be lifted to MacroView root (DEC6). C8 forbids any persistence layer. C9 mandates fully-expanded state at every mount. Together these uniquely determine the design captured in D6.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J3]", + "detail": null + }, + { + "local_id": 232, + "plane": "intent", + "kind": "context", + "title": "Multiple node-attribute encodings (border style for mode, border color for outcome, fill for phase, header chips for nudging/material progr…", + "body": "Multiple node-attribute encodings (border style for mode, border color for outcome, fill for phase, header chips for nudging/material progress, opacity for perspective selection, shape for impasse) coexist on a single node without ambiguity because each visual channel is reserved for a single semantic dimension.\n\n## Rationale\n\nG3/G4 require at-a-glance comprehension; X25, X27, X28, X32, X35, X23 each assign a different semantic attribute to a different visual channel; C7 forbids decorative color reuse; DEC8 explicitly orthogonalizes channels. Together these premises force the requirement that no two semantic attributes share the same visual channel, which is a property each node renderer must collectively satisfy.", + "basis": "explicit", + "source": "derived-justification-synthesis | [J1]", + "detail": null + } +] diff --git a/.fixtures/seed-specs/bilal-port/macro-view/spec.json b/.fixtures/seed-specs/bilal-port/macro-view/spec.json new file mode 100644 index 00000000..d8f6b9e0 --- /dev/null +++ b/.fixtures/seed-specs/bilal-port/macro-view/spec.json @@ -0,0 +1,5 @@ +{ + "slug": "macro-view", + "name": "Macro View", + "readiness_grade": "commitments_ready" +} diff --git a/.fixtures/workbenches/live-graph-observer/README.md b/.fixtures/workbenches/live-graph-observer/README.md index 24bf2717..e21db314 100644 --- a/.fixtures/workbenches/live-graph-observer/README.md +++ b/.fixtures/workbenches/live-graph-observer/README.md @@ -50,10 +50,11 @@ repo root. That state is per-cwd by design and must not be committed. ## Browser feedback loop -Use Chrome DevTools Protocol tooling as the primary browser observer. Playwright -can still be useful for future scripted cross-browser checks, but the branch's -manual feedback loop is CDP-first because it gives quick console, network, and -accessibility-tree inspection without becoming product runtime behavior. +Use `agent-browser` as the primary browser observer inside the agent-safehouse +sandbox. It keeps a daemon-backed Chrome instance alive across shell calls and +gives the agent accessibility-tree snapshots, clicks, form input, and screenshots +without becoming product runtime behavior. CDP-style tools remain useful for +console/network detail when needed. Launch the web host from this workbench: @@ -74,17 +75,26 @@ or, when no selected spec route is available yet: Brunch web sidecar listening on http://127.0.0.1: ``` -Open and inspect that URL with `cdp-cli`: +Open and inspect that URL with `agent-browser`: ```sh # Terminal B: browser observer -cdp-cli launch -cdp-cli new "http://127.0.0.1:/spec/" -cdp-cli tabs +agent-browser close 2>/dev/null || true +agent-browser --args "--no-sandbox,--ignore-certificate-errors" open "http://127.0.0.1:/spec/" + +# Accessibility tree / page content with stable refs such as @e1, @e2, ... +agent-browser snapshot + +# Optional interaction and visual capture +agent-browser click @e2 +agent-browser screenshot /tmp/brunch-live-graph-observer.png +``` -# Accessibility tree / page content -cdp-cli snapshot "127.0.0.1" -cdp-cli snapshot "127.0.0.1" --format text +If you need console or network detail rather than interaction/page structure, +attach a CDP-style tool to the same URL: + +```sh +cdp-cli tabs # Runtime signals cdp-cli console "127.0.0.1" -t error -d 2 @@ -105,12 +115,9 @@ code, that slice owns the dependency/import change. ### Current verification note -- `npm run build` passed during the Card 2 builder attempt. -- `brunch-cli --mode web` launched from this workbench and printed a localhost - URL. -- Browser automation is still pending on this machine until a local browser - backend works. Observed blockers: harness Chromium crashed, Playwright's - default Chrome path crashed under macOS sandbox/framework loading, - WebKit/Firefox were not installed, and the Chromium browser install attempt - was interrupted. Until that is fixed, the documented CDP loop is the intended - command path but the page-observable browser smoke is not yet complete. +- `npm run build` passed during the FE-795 tie-off check. +- `agent-browser` was verified on 2026-06-04 with the sandbox launch args above. +- A browser-observable FE-795 smoke opened a fresh selected-spec web dashboard, + observed empty graph state, committed a node through the default Brunch + runtime `commit_graph` tool path with the shared product-update bus, and + observed the browser update without page reload. diff --git a/.oxlintrc.json b/.oxlintrc.json index a471a1fe..79243621 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -5,7 +5,8 @@ "typeCheck": true }, "rules": { - "typescript/no-deprecated": "error" + "typescript/no-deprecated": "error", + "unicorn/no-empty-file": "off" }, "ignorePatterns": [ ".agents/**", diff --git a/docs/archive/PLAN_HISTORY.md b/docs/archive/PLAN_HISTORY.md index 3b324a3d..1a1d491a 100644 --- a/docs/archive/PLAN_HISTORY.md +++ b/docs/archive/PLAN_HISTORY.md @@ -3,6 +3,12 @@ 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-04 Rolling completion archive + +Archived from `memory/PLAN.md` when FE-795 tied off and the live frontier advanced to `agents-composition-layer`. + +- 2026-06-01 — **graph-data-plane** (FE-741) — Drizzle schema/init, graph clock seed, `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `commitGraph` atomic batch mutation, graph snapshot readers, and reconciliation-need substrate landed. Verified: `npm run verify`. Follow-on observer/capture/review pressure moved into the delivery frontiers that now consume the graph plane. + ## 2026-06-02 Delivery-cut archive Archived from `memory/PLAN.md` when the live plan shifted from concept/frontier proving to the POC delivery spine. The delivery plan now keeps only the P0/P1 marks and the last few completed summaries live. diff --git a/docs/design/GRAPH_MODEL.md b/docs/design/GRAPH_MODEL.md index 5a264640..fab8adbb 100644 --- a/docs/design/GRAPH_MODEL.md +++ b/docs/design/GRAPH_MODEL.md @@ -19,6 +19,10 @@ graph-model contract. - **Phase 1:** edges, edge policy, reconciliation-need shape. Locked. - **Phase 2:** per-plane node kinds, node shape, detail schemas, kind categories, `source` field, `provenance` retirement. Locked. +- **Current lock:** stable node reference codes, `basis` as + approval strength (`explicit | implicit`), non-exclusive + readiness bands, supersession acyclicity, and snapshot + graph-truth vs active-context separation. Locked. ## Scope and posture @@ -43,11 +47,40 @@ policy. ## Atoms ```ts -type NodeId = string -type EdgeId = string -type Lsn = number // monotonic, one per commit +type SpecId = number +type NodeId = number // SQLite integer primary key / FK +type EdgeId = number // SQLite integer primary key / FK +type KindOrdinal = number // monotonic per (spec, plane, kind) +type Lsn = number // monotonic, one per commit ``` +`NodeId` and `EdgeId` are internal storage identities. The database +stores `kindOrdinal`, not a rendered reference-code string. Human +and agent-facing references use projected codes such as `R3`, derived +from `node.kind` plus `kindOrdinal` through a hard-coded presentation +lookup (see [§Stable node reference codes](#stable-node-reference-codes)). + +## Graph basis — approval strength, not mutation path + +```ts +type GraphBasis = "explicit" | "implicit" +``` + +`basis` is shared by nodes and edges. It records whether the exact +accepted graph item was user-approved: + +- **`explicit`** — the user directly stated the node/edge, or + approved that exact node/edge in a review set. +- **`implicit`** — the user accepted a concept/proposal, and the + agent materialized specific graph items to match it without + per-item review (the `propose-graph` direct-commit path). + +`basis` does **not** record the mutation pathway. The pathway lives +in `change_log.operation` and payload (`commit_graph`, +`accept_review_set`, post-exchange capture, etc.). Low-confidence +inferred material still stays outside graph truth until clarified or +accepted. + ## GraphEdge — the single shape ```ts @@ -62,10 +95,11 @@ type EdgeCategory = | "supersession" type EdgeStance = "for" | "against" // required for proof | support -type EdgeBasis = "explicit" | "accepted_review_set" +type EdgeBasis = GraphBasis interface GraphEdge { readonly id: EdgeId + readonly specId: SpecId readonly category: EdgeCategory readonly sourceId: NodeId readonly targetId: NodeId @@ -100,8 +134,10 @@ named successor home; nothing is lost, the substrates are different. | `family` | Implied by category | | Per-relation policy axes | Per-category policy table below | -Authority for edge writes lives in the `change_log` keyed by -`createdAtLsn` / `updatedAtLsn`. Edges do not denormalize authority. +Audit for edge writes lives in the `change_log` keyed by +`createdAtLsn` / `updatedAtLsn`. Edges do not denormalize transcript +pointers or mutation pathway. Their `basis` only records item-level +approval strength. ## Edge categories — directional first @@ -212,8 +248,8 @@ M_sqlite_store : module -[composition]-> M_sqlite_helper: module M_sqlite_store : module -[dependency]-> M_pi_session : module # Plan (M5+ stub) -MS_graph : milestone -[composition]-> FE_700 : frontier -FE_700 : frontier -[composition]-> SL_persist : slice +MS_graph : milestone -[composition]-> FR_graph_data : frontier +FR_graph_data: frontier -[composition]-> SL_persist : slice R_offline : requirement -[realization]-> SL_persist : slice SL_persist : slice -[supersession]-> SL_persist_v0 : slice ``` @@ -304,33 +340,61 @@ more realization sub-clusters that demand distinct cascade or projection policy, split `realization` into siblings (see [§Open questions](#open-questions)). -## Snapshot bucketing +## Snapshot projections and bucketing Snapshot buckets come from category and endpoint role, not from the -derived label string. A neighborhood snapshot of an intent node: +derived label string. Snapshot callers must also choose which +projection they want: + +- **`graph_truth`** — accepted graph truth records. Superseded + predecessors and their edges may still appear because they are + part of auditably accepted graph state. +- **`active_context`** — the context the agent/user should treat as + current. Superseded predecessor nodes are hidden, and edges whose + endpoints are hidden are also omitted so active-context snapshots + never contain dangling references. + +The read family should stay product-shaped and close to observed +needs, not become a generic records API: + +```ts +listNodes({ kinds?, readinessBands?, basis?, activeOnly? }) + +relatedNodes({ + anchors, + edgeCategories?, + direction: "incoming" | "outgoing" | "both", + hops?, + projection?: "graph_truth" | "active_context", +}) + +overview({ projection: "graph_truth" | "active_context" }) +``` + +A neighborhood snapshot of an intent node: ```text -anchor: R_offline : requirement +anchor: R1 : requirement hard dependencies: - A_no_network depends on assumption + A1 depends on assumption support: - P_field_users motivated by context + CTX2 motivated by context proof: - CR_airplane witnessed by criterion - E_typical witnessed by example + CR1 witnessed by criterion + EX1 witnessed by example realized by: - M_sqlite_store realized by design module - SL_persist established by plan slice + M1 realized by design module + SL1 established by plan slice boundaries: - C_no_cloud bounded by constraint + CON1 bounded by constraint supersedes: - R_offline_v0 supersedes prior requirement + R0 supersedes prior requirement ``` ## Structural invariants @@ -339,21 +403,32 @@ supersedes: relation strings. - Every edge has exactly one category. - `stance` is required iff `category ∈ { proof, support }`. +- `basis` is exactly `explicit | implicit` for accepted nodes and + edges; mutation path is recovered from `change_log`, not from + `basis`. +- Every node has a stable `kindOrdinal`; rendered reference codes are + a projection from `kind` + `kindOrdinal`, not stored graph state. +- `(specId, plane, kind, kindOrdinal)` is unique, and ordinals are + monotonic / never reused for that tuple. - `association` is symmetric at the product level even if stored with `sourceId` / `targetId` columns. -- `supersession` chains are acyclic. +- `supersession` chains are acyclic. CommandExecutor validation + checks proposed supersession edges together with existing edges. - Accepted graph edges are graph truth. Candidate or low-confidence edges live outside graph truth (preface / capture analysis / review-set drafts) until accepted. - Tuple-label lookup cannot change category policy. - Snapshot bucket assignment comes from category and endpoint role, not from label strings. +- Active-context snapshots omit superseded nodes and any edge whose + endpoint is omitted. - `composition` does not imply sequencing or dependency. - `support` does not imply blocking / staleness by default. - Only `dependency` triggers automatic cascades; other categories surface as `ReconciliationNeed` records when policy says so. - Cross-plane freedom: node `kind` does not constrain edge category legality. +- Readiness bands do not constrain node creation legality. ## Agent-facing command surface @@ -375,7 +450,10 @@ linkSupersession({ successor, predecessor, basis, rationale }) The command layer owns structural validation. If a tuple is structurally illegal (missing stance, supersession cycle, etc.) the tool returns `structural_illegal`; the agent should not invent a -narrower category to force the write through. +narrower category to force the write through. In most agent-facing +flows, `basis` is supplied by the strategy adapter or execution +context (`explicit` for exact user/review approval, `implicit` for +`propose-graph` materialization), not improvised per edge. These commands land in the M5 `agent-graph-integration` extension under `src/.pi/extensions/graph/tools/` per D52-L. They are out of @@ -385,32 +463,42 @@ scope for Phase 1 stubs. The `propose-graph` strategy's load-bearing tool. One tool call creates an entire subgraph — nodes and edges — in a single -transaction with one LSN. +transaction with one LSN. Direct `propose-graph` commits use +`basis: "implicit"` because the user accepted a concept, not each +individual item. Review-set acceptance is a parallel path to the +same executor and uses `basis: "explicit"` because the user approved +the exact reviewed items. ```ts commitGraph({ + basis: "implicit", nodes: [ - { ref: "n1", kind: "requirement", title: "...", body: "..." }, - { ref: "n2", kind: "constraint", title: "...", body: "..." }, - { ref: "n5", kind: "invariant", title: "...", body: "..." }, - { ref: "n3", kind: "decision", title: "...", body: "...", + { ref: "n1", plane: "intent", kind: "requirement", title: "...", body: "..." }, + { ref: "n2", plane: "intent", kind: "constraint", title: "...", body: "..." }, + { ref: "n5", plane: "intent", kind: "invariant", title: "...", body: "..." }, + { ref: "n3", plane: "intent", kind: "decision", title: "...", body: "...", detail: { chosen_option: "...", rejected: ["..."], rationale: "..." } }, - { ref: "n4", kind: "term", title: "...", + { ref: "n4", plane: "intent", kind: "term", title: "...", detail: { definition: "...", aliases: ["..."] } }, ], edges: [ - { category: "dependency", source: "n1", target: "n2" }, - { category: "boundary", source: "n2", target: "n1" }, - { category: "realization", source: "n1", target: "n3" }, - { category: "support", source: { existing: "A12" }, target: "n1", + { category: "dependency", source: "n1", target: "n2" }, + { category: "boundary", source: "n2", target: "n1" }, + { category: "realization", source: "n1", target: "n3" }, + { category: "support", source: { existingCode: "A1" }, target: "n1", stance: "for" }, ] }) ``` Reference modes: -- **Intra-batch**: `"n1"` — a node defined in the same payload -- **Existing**: `{ existing: "A12" }` — a node already in the graph + +- **Intra-batch**: `"n1"` — a node defined in the same payload. +- **Existing**: `{ existingCode: "A1" }` — a node already in the + selected spec, addressed by a projected reference code. Tool + adapters parse this to `kind` + `kindOrdinal` and resolve the + numeric `NodeId` before calling lower-level executor helpers; graph + tables do not store the rendered code string. CommandExecutor processing: @@ -419,13 +507,14 @@ commitGraph tool call │ ▼ 1. Validate all nodes structurally - 2. Assign real NodeIds to each batch ref - 3. Resolve intra-batch refs on edges - 4. Resolve existing-node refs (fail if not found) - 5. Validate all edges (closed categories, stance, acyclicity) - 6. Allocate ONE Lsn - 7. Write all nodes + edges + change-log in one transaction - 8. Return success + created ids + 2. Allocate ONE Lsn + 3. Allocate per-kind ordinals + 4. Insert nodes and build batch ref → NodeId/kindOrdinal map + 5. Resolve intra-batch refs on edges + 6. Resolve existing-node refs (fail if not found or wrong spec) + 7. Validate all edges (closed categories, stance, supersession acyclicity) + 8. Write all nodes + edges + change-log in one transaction + 9. Return success + created ids/kindOrdinals (adapters may render codes) OR structural_illegal + diagnostics for retry ``` @@ -456,13 +545,21 @@ reconciliation_need. Use one edge for the strongest operational role between two nodes. Do not create multiple edges merely because several English paraphrases are possible. + +Basis rule: use explicit only when the user directly stated the item or approved the exact +node/edge in a review set. Use implicit for propose-graph commits where the user accepted +the concept but did not review each graph item. Do not use accepted_review_set as a basis. + +Readiness rule: readiness grade and readiness bands guide what to ask for next; they do not +forbid capturing clear requirements, criteria, checks, or design nodes early. ``` Category-selection rubric (ask in order; stop at first strong match): ```text 0. Should this be graph truth now? - - explicit user statement, accepted review set, or high-confidence extraction -> continue + - explicit user statement, exact accepted review set item, high-confidence extraction, + or accepted propose-graph concept with clear materialization -> continue - weak inference, possible relation, possible duplicate, unresolved ambiguity -> no accepted edge 1. Is a newer item intentionally replacing an older item for overlapping scope? @@ -507,29 +604,38 @@ Category-selection rubric (ask in order; stop at first strong match): ```ts interface GraphNode { - readonly id: NodeId + readonly id: NodeId // internal SQLite identity + readonly specId: SpecId + readonly kindOrdinal: KindOrdinal // per (spec, plane, kind) readonly plane: NodePlane - readonly kind: string // per-plane closed enum (see below) - readonly title: string // required, non-empty - readonly body?: string // markdown content + readonly kind: string // per-plane closed enum (see below) + readonly title: string // required, non-empty + readonly body?: string // markdown content readonly basis: NodeBasis - readonly source?: string // free-form epistemic attribution + readonly source?: string // free-form epistemic attribution // convention by prompt, not structural validation // e.g. "stakeholder", "regulatory", "derived" - readonly detail?: object // per-kind validated sub-structure (JSON column) + readonly detail?: object // per-kind validated sub-structure (JSON column) readonly createdAtLsn: Lsn readonly updatedAtLsn: Lsn } type NodePlane = "intent" | "oracle" | "design" | "plan" -type NodeBasis = "explicit" | "accepted_review_set" -// Same semantics as EdgeBasis — how the node entered graph truth. +type NodeBasis = GraphBasis +// Same semantics as EdgeBasis — item-level approval strength. // No "inferred" basis; low-confidence material stays in preface / // capture analysis until promoted. ``` ### Fields +- **`id`** — internal storage/FK identity. It is stable, but not the + primary human or agent-facing handle. +- **`specId`** — selected-spec owner. Ordinals and projected + reference codes are scoped by spec. +- **`kindOrdinal`** — monotonic integer per `(specId, plane, kind)`; + never reused after deletion or supersession. The rendered human + reference code is derived later from `kind` + `kindOrdinal`. - **`plane`** — which graph plane owns this node. Structurally validated; determines which `kind` enum applies. - **`kind`** — per-plane closed enum. Structurally validated by @@ -538,8 +644,8 @@ type NodeBasis = "explicit" | "accepted_review_set" node. Used for mentions, snapshot display, and search. - **`body`** — optional markdown content. Carries the semantic detail the agent authored. Most kinds put their primary content here. -- **`basis`** — how the node entered graph truth. Same `explicit` / - `accepted_review_set` semantics as edges. +- **`basis`** — item-level approval strength: `explicit` or + `implicit`. See [§Graph basis](#graph-basis--approval-strength-not-mutation-path). - **`source`** — free-form string for epistemic attribution. Convention by prompt (e.g. "stakeholder", "regulatory", "derived", "domain expert", "market research", "agent synthesis"), not @@ -553,13 +659,46 @@ type NodeBasis = "explicit" | "accepted_review_set" entryId, proposalEntryId) are fragile under compaction and redundant with `change_log` + `basis`. +## Stable node reference codes + +Node reference codes are the human/agent handle for accepted graph +nodes. They are spec-scoped and stable for the life of the node, but +they are **not stored** in the graph tables: + +```ts +referenceCode = NODE_KIND_LABELS[node.kind] + node.kindOrdinal +``` + +`NODE_KIND_LABELS` is a hard-coded presentation lookup used by UI, +prompt-context renderers, and agent-tool adapters. If code needs an +internal key for lookup, use the canonical `node.kind` string plus +`kindOrdinal`, not the rendered reference-code string. + +Allocation rules: + +1. Prefix labels are presentation metadata and unique across all node + kinds so `#`-mention parsing can use longest-prefix matching. +2. `kindOrdinal` is allocated monotonically per `(specId, plane, + kind)` inside the same CommandExecutor transaction that creates + the node. +3. Allocation uses a counter table (`node_kind_counters` or + equivalent), not `MAX(kind_ordinal)+1`, so deletion and + supersession cannot reuse ordinals. +4. DB constraints enforce `unique(spec_id, plane, kind, kind_ordinal)`. + There is no `code` column and no `unique(spec_id, code)` database + constraint. +5. Snapshots and prompts render projected codes as primary handles. + Raw IDs may appear in diagnostics, but product/agent references + should use projected codes. + ## Per-plane node kinds ### Intent plane -Intent kinds fall into three **derived categories** that map to -spec-grade progression. Category is a pure function of `kind` — it -is not stored on the node. +Intent kinds fall into three **derived semantic categories**. +Category is a pure function of `kind` — it is not stored on the node. +These semantic categories are distinct from the cross-plane +readiness bands in [§Node kind metadata](#node-kind-metadata-codes-and-readiness-bands). | Category | Kind | Modality of claim | Source question | | --- | --- | --- | --- | @@ -587,14 +726,11 @@ for what kind of material the node captures. **Category semantics:** - **`basic`** — grounding material. Establishes what/who/why before - structural elicitation can proceed. The spec-grade gate from - `grounding_onboarding` toward `elicitation_ready` requires a - satisficing threshold of `basic`-category nodes. The gate is - LLM-judged with a count floor — the agent assesses readiness, - but cannot declare grounding complete with zero basic nodes. - Grounding rubric (Walter-style questions: what is it, who is it - for, what problem, what value, when used, how measured) lives in - the prompt as abstract drivers, not structural enforcement. + structural elicitation can proceed. It is semantic, not a creation + gate. The spec-grade gate from `grounding_onboarding` toward + `elicitation_ready` uses readiness-band evidence with a count + floor; basic intent nodes are central evidence, and + grounding-relevant constraints may also count. - **`structural`** — core specification material. Requirements, assumptions, and constraints form the structural backbone. - **`reasoning`** — decisions, criteria, and evidence. Emerges as @@ -624,6 +760,52 @@ for what kind of material the node captures. | `frontier` | A named canonical work item within a milestone | | `slice` | A thin vertical implementation unit within a frontier | +## Node kind metadata: codes and readiness bands + +Metadata is a pure function of `(plane, kind)`. It is not stored as a +nested object on each node. Readiness-band membership is consumed by +snapshot / prompt filters; reference-code labels are consumed by +presentation code that combines the label with stored `kindOrdinal`. + +Readiness bands are **non-exclusive**. They guide elicitor goals, +snapshot filters, and grade-advancement rubrics; they do not make any +node kind illegal at earlier grades. If the user clearly states a +requirement or criterion during grounding, capture it as graph truth +with the right `basis`; it simply does not by itself prove the +readiness threshold. + +| Plane | Kind | Prefix | Readiness bands | +| --- | --- | --- | --- | +| intent | `goal` | `G` | grounding | +| intent | `thesis` | `TH` | grounding | +| intent | `term` | `T` | grounding | +| intent | `context` | `CTX` | grounding | +| intent | `assumption` | `A` | elicitation | +| intent | `constraint` | `CON` | grounding, elicitation | +| intent | `invariant` | `I` | elicitation | +| intent | `decision` | `D` | elicitation | +| intent | `example` | `EX` | elicitation | +| intent | `criterion` | `CR` | commitment | +| intent | `requirement` | `R` | commitment | +| oracle | `validation_method` | `VM` | elicitation | +| oracle | `obligation` | `OB` | elicitation | +| oracle | `evidence` | `EV` | commitment | +| oracle | `check` | `CH` | commitment | +| design | `module` | `M` | elicitation | +| design | `interface` | `IF` | elicitation | +| plan | `milestone` | `MS` | commitment | +| plan | `frontier` | `FR` | commitment | +| plan | `slice` | `SL` | commitment | + +Notes: + +- `criterion` uses `CR`, not the previous app's legacy `AC`, because + Brunch-next treats it as an intent/oracle claim rather than a + phase-specific acceptance-criteria record. +- Prefixes are 1–3 capital letters and must remain globally unique + across node kinds. If a new kind would collide by prefix, choose a + longer prefix rather than changing existing codes. + ## Per-kind detail schemas Most kinds use `title` + `body` only. Two kinds have structured @@ -662,10 +844,12 @@ rubric. Additional prompting heuristics for kinds that need them: (stated directly by a stakeholder, `source: "stakeholder"`, `basis: "explicit"`) or projection-shaped (derived from existing goals/theses/constraints via `project-graph`, `source: "derived"`, - `basis: "accepted_review_set"`). Both are obligation claims. The - `source` and `basis` fields carry the provenance distinction; - strategy prompt packs (`step-wise` vs `project-graph`) guide the - agent on which framing to use. + `basis: "explicit"` after exact review-set approval). A + `propose-graph` requirement is also an obligation claim, but its + `basis` is `implicit` because the user accepted the concept rather + than each item. `source` carries epistemic attribution; + `basis` carries item-level approval strength; `change_log` carries + the mutation path. - **`decision` capture criteria.** A claim should become a `decision` only if all of the following hold: 1. **Plausible alternatives existed** — "we chose A over B" @@ -703,6 +887,32 @@ rubric. Additional prompting heuristics for kinds that need them: | is a bet about users/market/value | `thesis` | | just helps interpretation | keep as `context` | +- **Interrogative content normalization.** Brunch has no + `question` kind — every intent node is a declarative claim. + When elicitation produces interrogative material ("Open + question: …", "Should we …?", "Is X true?"), rewrite into the + underlying declarative claim before authoring. Reserve graph + truth for what the question is *about*; track the question + itself, if it needs tracking, outside the graph as a worklist + or capture artifact, not as a node. Common rewrites: + + | If the question is about… | Author as… | + | --- | --- | + | a possibly-false premise downstream depends on | `assumption` (rewrite as the latent premise) | + | how success or correctness will be judged | `criterion` (rewrite as the judgment claim) | + | which option to take among alternatives, still open | `context` (state that the choice is unresolved; preserve original wording in `body`) | + | a follow-up task with no stable declarative content yet | keep outside graph truth | + + When such an unresolved-state `context` node is later resolved, + create a fresh `decision` and link + `decision -[supersession]-> context`. This preserves the + discovery-to-resolution arc without mutating either node and + without a dedicated `question` kind. Note that interrogative + rewriting is independent of the + `present_question` / `present_options` structured-exchange + surface — interrogatives are valid at the *prompt* layer; this + rule constrains only what enters the *graph*. + ### Beyond the schema contract Two categories of agent-facing guidance live outside this document @@ -775,14 +985,21 @@ This document supersedes: that produced this document - The `framing_as` orthogonal modality and allowed matrix from `memory/SPEC.md` D7-L, A7-L, I7-L — absorbed by `thesis`, - `term`, `constraint.subtype`, and `goal` + `term`, `constraint`, and `goal` - `EdgeProvenance` / node provenance — retired; `change_log` owns audit trail - -Outbound references updated with Phase 2 lock: - -- `memory/SPEC.md` — D54-L (node shape), D55-L (provenance - retirement), D56-L (intent kind categories), D57-L (grounding - gate); A7-L retired; I7-L retired; I36-L, I37-L added -- `memory/PLAN.md` — `sealed-pi-profile-runtime-state` Phase 2 - node lock acceptance criteria updated +- The former `basis: accepted_review_set` path value — replaced by + `basis: explicit | implicit`, with mutation path in `change_log` +- String-shaped `NodeId` examples — replaced by integer internal ids + plus projected human reference-code handles derived from `kind` + + `kindOrdinal` + +Outbound references updated with current graph-model lock: + +- `memory/SPEC.md` — D51-L (edge shape), D54-L (node shape), D55-L + (provenance retirement), D56-L (intent kind categories), D57-L + (grounding gate), D62-L (projected node reference codes), D63-L (basis), D64-L + (readiness bands); A7-L retired; I7-L retired; I36-L, I37-L, + I39-L, I40-L, I41-L added +- `memory/PLAN.md` — frontier traceability still points at the graph + write/capture/review-cycle work that must materialize these locks diff --git a/docs/design/REVIEW_SETS.md b/docs/design/REVIEW_SETS.md index 1dfa7234..9123cd6a 100644 --- a/docs/design/REVIEW_SETS.md +++ b/docs/design/REVIEW_SETS.md @@ -20,40 +20,40 @@ This pattern is **reusable across generative lenses**: the same mechanism that h ## Proposal payload shape -Generative-lens proposals carry **structured entity-draft payloads** in the proposal custom entry. The proposal contains the graph entities and edges that *would* be created on acceptance, in a form `CommandExecutor` can validate without re-parsing. +Generative-lens proposals carry **structured entity-draft payloads** inside the `present_review_set` / `request_review` structured exchange. The proposal contains the graph entities and edges that *would* be created on acceptance, in a form `CommandExecutor` can validate without re-parsing. Edge drafts follow the locked graph contract from [GRAPH_MODEL.md](GRAPH_MODEL.md): closed `category` values, optional `stance` only for `proof`/`support`, and -`basis: "accepted_review_set"` for proposal-time edges. Review-set payloads no -longer carry a free-form `relation` string. +projected existing-node codes at adapter/UI boundaries instead of raw DB ids. +Review-set payloads no longer carry a free-form `relation` string or a +`basis: "accepted_review_set"` path value. Acceptance commits exact reviewed +items with `basis: "explicit"`; the mutation path lives in `change_log`. Approximate shape (refined during M5 implementation): ```text { - customType: "brunch.review_set_proposal", - payload: { + present_review_set: { lens: "propose-scenarios-with-tradeoffs", epistemic_status: "asserted", proposal_version: 2, - supersedes: "", // null on first proposal + supersedes: "", // null on first proposal pitch: { name: "...", narrative: "...", anchor_scenarios: [ { title, vignette }, ... ] }, entity_drafts: [ - { draft_id, kind: "intent_node", framing_as: "problem", title, body }, - { draft_id, kind: "intent_node", framing_as: "persona", title, body }, + { draft_id, plane: "intent", kind: "thesis", title, body }, + { draft_id, plane: "intent", kind: "requirement", title, body }, ... ], edge_drafts: [ { category: "support", - source_draft_id: "persona-1", + source_draft_id: "thesis-1", target_draft_id: "requirement-2", stance: "for", - basis: "accepted_review_set", }, ... ], diff --git a/docs/praxis/ln-skills.md b/docs/praxis/ln-skills.md index adfe8a43..705dee57 100644 --- a/docs/praxis/ln-skills.md +++ b/docs/praxis/ln-skills.md @@ -34,7 +34,16 @@ ln-consult The flow is not a checklist. Skip steps whose uncertainty is already retired. -### Tracer-bullet sequencing +### Operating posture + +Planning and scoping pressures depend on each frontier's **certainty posture**. The project default lives in `.pi/POSTURE.md` (`certainty: proving | earned`); individual frontiers in `memory/PLAN.md` may carry an explicit `Certainty:` override. Posture is **per-frontier**, not per-project — a mostly-earned repo can carry a fresh proving seam, and a settled seam can regress to proving on a new unknown. + +| Certainty | Ask | Optimize for | Reference | +| --- | --- | --- | --- | +| `proving` | What does landing this *tell us*? | information gain | `.agents/skills/ln-plan/references/proving.md` | +| `earned` | What does landing this *close*? | closure gain | `.agents/skills/ln-plan/references/earned.md` | + +#### Proving posture (tracer-bullet sequencing) A good tracer-bullet frontier or slice earns its keep on three convergent axes: @@ -46,9 +55,27 @@ The strongest next move scores on more than one axis. Prefer a slice that does s - **Reshape, don't defer.** If an assumption blocks a slice, reshape the slice before switching to study. - **Spike exception.** Use `ln-spike` only when no buildable tracer bullet can carry the proof — a third-party API contract, vendor characteristic, or research-grade unknown. -- **Fire the tracer that tells you the most.** Given the repo's pre-release posture, attack uncertainty by building. Spikes, design passes, and prototypes are escape hatches when no slice could carry the proof more cheaply. +- **Fire the tracer that tells you the most.** Under proving posture, attack uncertainty by building. Spikes, design passes, and prototypes are escape hatches when no slice could carry the proof more cheaply. + +Required annotation fields on Active/Next frontiers: at least one of `Retires`, `Depends on`, `Blocked by`, `Lights up`, `Stabilizes`. + +#### Earned posture (closure sequencing) + +A good closure frontier or slice eliminates open shape, hardens a settled decision into topology, or retires an obsolete carrier. The decision kernel changes — the planner asks *what does this close?*, not *what does this tell us?* + +Closure move-set: **materialize, consolidate, name canonically, delete-as-progress, retire bridges/aliases/dual paths, take-the-bigger-step.** + +The "circling" recognition heuristic: when each new slice attaches an incremental proof to changes whose meaning is already established, and "caution" is the planner's stated reason but no specific risk can be named, switch posture and plan the closure move the proving slices have been deferring. + +Required annotation fields on Active/Next frontiers: at least one of `Closes`, `Materializes`, `Canonicalizes`, `Deletes / retires`, `Locks in`. + +Earned posture is not a license for sprawl — guardrails (one named seam, named closure target, declared touched paths, no auto-implementation of adjacent work) still bind. Topology READMEs and fractal sub-tree splits fire only when the seam is understood and structure carries real architectural meaning, not as ritual. + +Regression earned → proving is a state transition, not a third mode: downgrade the frontier or slice, reshape as a tracer, route back through `ln-plan` if the frontier itself splits. + +#### Posture distribution across skills -`ln-plan`, `ln-design`, `ln-scope`, and `ln-consult` all carry this sequencing pressure. +`ln-plan`, `ln-design`, `ln-scope`, and `ln-consult` all carry posture-dependent sequencing pressure. `ln-plan` reads posture and loads the matching reference; `ln-scope` inherits posture from the containing frontier and applies the matching posture check. `ln-refactor` owns closure as safe mechanics (when an earned frontier is principally restructuring); `ln-sync` owns closure as canonical garbage collection (when artifacts the planner is already done with need cleanup). ## Skill map diff --git a/memory/PLAN.md b/memory/PLAN.md index ece4e0a0..ad4943b8 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -15,9 +15,9 @@ Brunch-next is now in a **POC delivery cut**. The earlier concept-driven frontier work proved the host, transcript, public RPC, sealed Pi profile, SQLite graph data plane, `CommandExecutor`, real graph tools, and one real `propose-graph → commitGraph` agent proof. The remaining POC work is not to prove Brunch is good at specification work in the broad product-quality sense; that belongs beyond this POC. The delivery question is narrower and stricter: can the real product entrypoints compose without the harness secretly supplying wiring? -The black triangles for this cut are: +The delivery cut's black triangles are (live graph observability is now landed; the rest remain in sequence): -1. **Live graph observability:** the TUI remains the writer/agent session while the web app attaches over Brunch WebSocket RPC and shows the selected spec's graph changing. +1. **Live graph observability (landed):** the TUI remains the writer/agent session while the web app attaches over Brunch WebSocket RPC and shows the selected spec's graph changing. 2. **Behavioral runtime posture:** operational goal/strategy/lens state changes the actual prompt/resource/tool posture, not just a stored label. 3. **Capture to graph truth:** a structured elicitation response can become high-confidence graph truth through `CommandExecutor`, visible to web/TUI projections. 4. **Graph tool resilience:** the direct agent graph path survives more than the one A14 happy path: existing-node refs, structural-illegal diagnostics/retry, and ambiguity/no-overcommit cases. @@ -31,16 +31,15 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple ### Active -- None — `live-graph-observer` is tied off on FE-795; next action is to start `agents-composition-layer` on its Graphite branch. +_None._ ### Next -1. `agents-composition-layer` — P0 behavior black triangle: make goal/strategy/lens/grade/posture change prompt manifests and agent posture; retire `src/.pi/context` into `src/agents`. +1. `graph-tool-resilience` — P0 structural hardening: materialize the locked graph write contract (projected node codes, explicit/implicit basis, supersession acyclicity) before more graph-writing frontiers build on stale schema. 2. `capture-response-to-graph` — P0 product loop: structured exchange answer → narrow high-confidence capture → `CommandExecutor` commit → web graph update. -3. `graph-tool-resilience` — P0 hardening: broaden the A14 proof beyond the single happy path and capture retry/diagnostic evidence. -4. `project-graph-review-cycle` — P1 unless demo narrative promotes it: real `project-graph` review-set proposal/approval loop. -5. `minimal-authority-shell` — P1 safety: thin POC authority posture over already-existing command-result seams and `elicit` tool policy. -6. `poc-live-ship-gate` — P1 final gate: fresh-cwd runbook exercising the composed product path end to end. +3. `project-graph-review-cycle` — P1 unless demo narrative promotes it: real `project-graph` review-set proposal/approval loop. +4. `minimal-authority-shell` — P1 safety: thin POC authority posture over already-existing command-result seams and `elicit` tool policy. +5. `poc-live-ship-gate` — P1 final gate: fresh-cwd runbook exercising the composed product path end to end. ### Parallel / Low-conflict @@ -60,34 +59,13 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple ## Frontier Definitions -### live-graph-observer - -- **Name:** Live selected-spec graph observer over web RPC -- **Linear:** [FE-795](https://linear.app/hash/issue/FE-795/live-selected-spec-graph-observer-over-web-rpc) -- **Branch:** `ln/fe-795-live-over-web-rpc` -- **Kind:** bounded feature / tracer bullet -- **Status:** done — tied off 2026-06-04. -- **Objective:** Make the graph visible as live product state while the TUI remains the writer. Add product RPC graph reads/subscriptions and a minimal web graph panel so a graph mutation from the TUI/agent path updates the browser's selected-spec graph view. -- **Why now / unlocks:** This is the primary POC observability mark. Without a simultaneous TUI session and web graph view, the graph-native workspace remains mostly invisible even though persistence and tools exist. -- **Acceptance:** - - `graph.overview` and a focused graph read such as `graph.nodeNeighborhood` or equivalent target names are exposed through Brunch JSON-RPC discovery with schemas/examples. - - Web attaches over the existing WebSocket RPC client, selects/uses explicit `{specId, sessionId?}` product resources, and renders a minimal intelligible graph projection (list/table/graph visualization is acceptable; polish is not the point). - - A graph commit made through the real product path invalidates/notifies the web client, which refetches from canonical graph readers and shows the updated selected-spec graph without page reload. - - The TUI remains the writer; the web surface is read-only unless a later explicit product command changes that. - - Multi-spec discipline: graph reads target the selected/current spec; no workspace-global graph projection is introduced. -- **Verification:** Inner — RPC handler/discovery/schema tests; web query/render tests around empty graph and populated graph. Middle — integration test/probe evidence performs a real graph-tool write and observes `brunch.updated` notification/refetch over the public RPC surface. Outer — 2026-06-04 browser-observable `agent-browser` smoke opened a fresh selected-spec web dashboard, observed empty graph state, committed a node through the default Brunch runtime `commit_graph` tool path with the shared product-update bus, and observed the browser update without page reload. Literal keyboard-driven TUI smoke was not rerun at tie-off; `brunch-tui.test.ts` covers the TUI launch path starting the same read-only sidecar with the shared publisher. -- **Topology materialization:** `graph/` remains the read/domain owner; `session/` owns transcript-backed runtime-state projection; `rpc/` owns graph/session method handlers plus the process-local product update publisher; `web/` owns graph rendering, route loaders, Query keys, and notification invalidation; no `web/` or `.pi/` import of `db/`; no duplicate graph/runtime DTOs outside projected/read-model types. -- **Cross-cutting obligations:** Preserve D19-L thin named RPC methods, D33-L client attachment semantics, D35-L product chrome/projection discipline, D40-L transcript-backed runtime state, and D52-L source dependency direction. `brunch.updated` is an invalidation hint over transports, not canonical truth or a durable event store. Do not introduce a generic read gateway or view store. -- **Traceability:** R7, R10, R11, R12 / D5-L, D10-L, D19-L, D33-L, D40-L, D52-L, D60-L / I21-L, I25-L, I35-L / A3-L, A4-L. -- **Design docs:** `memory/SPEC.md` D19-L, D33-L, D40-L, D52-L, D60-L; `src/rpc/README.md`; `src/session/README.md`; `src/web/README.md`; `docs/design/GRAPH_MODEL.md`. - ### agents-composition-layer - **Name:** Agent prompt-resource composition, runtime manifests, and snapshot contexts - **Linear:** [FE-806](https://linear.app/hash/issue/FE-806/agent-prompt-resource-composition-runtime-manifests-and-snapshot) -- **Branch:** to create — `ln/fe-806-agents-composition-layer` +- **Branch:** `ln/fe-806-agents-composition-layer` - **Kind:** structural -- **Status:** next +- **Status:** done - **Objective:** Build the D58-L/D59-L/D60-L `agents/` layer so runtime state changes behavior: `agents/state.ts` legal tuples and resource manifest metadata; `agents/compose.ts` runtime header + gated manifests; Brunch-owned markdown resources for definitions/goals/strategies/lenses/methods; agent-context snapshot renderers; and migration/deletion of the old `src/.pi/context` composer. - **Why now / unlocks:** Runtime vocabulary has landed, but stored axes are not enough. The POC needs switchable strategies/lenses/goals to change prompt posture and available resources before capture and review-cycle behavior can be judged plausibly. - **Acceptance:** @@ -101,7 +79,7 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple - **Cross-cutting obligations:** Preserve D39-L sealed resource policy: manifest metadata is code-owned, not filesystem-discovered. Workspace posture is workspace-scoped header input, not spec/session/graph truth. Multi-spec discipline: composition reads the selected spec's grade/graph snapshots only. - **Traceability:** D25-L, D39-L, D40-L, D52-L, D58-L, D59-L, D60-L / I18-L, I33-L, I35-L, I38-L / A14-L, A22-L. - **Design docs:** `memory/SPEC.md` §Prompt/runtime profile architecture; `src/agents/README.md`; `src/.pi/README.md`. -- **Current execution pointer:** First slice should be `agents/state.ts` + `compose.ts` skeleton over the landed runtime vocabulary, then minimal P0 resource authoring, then snapshots, then deletion of `src/.pi/context`. +- **Current execution pointer:** Complete. Prompt manifests, selected-spec context renderers, product prompt-path snapshot wiring, legacy `.pi/context` deletion, and deterministic runtime-posture proof are landed. ### capture-response-to-graph @@ -110,39 +88,52 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple - **Branch:** to create — `ln/fe-807-capture-response-to-graph` - **Kind:** structural / tracer bullet - **Status:** next +- **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. - **Objective:** Prove the single-exchange path: a typed structured-exchange response is captured synchronously into high-confidence graph mutations through `CommandExecutor`, and the resulting graph change is visible through web/TUI projections. - **Why now / unlocks:** Structured exchanges and graph commits work separately. This frontier makes elicitation actually graph-native for the POC. It directly attacks A22-L while preserving the single mutation authority. - **Acceptance:** - A narrow capture path exists for 2–4 high-confidence intent facts, starting with basic/grounding kinds such as `goal`, `context`, `constraint`, `criterion`, or `assumption`; low-confidence implications remain out of graph truth and can be rendered as preface/disambiguation material. - Capture targets the spec bound to the session's `brunch.session_binding`; it never writes to a workspace-global graph or an unbound/default spec. - - Captured graph mutations route only through `CommandExecutor` and produce normal LSN/change-log entries. + - Captured graph mutations route only through `CommandExecutor`, write directly stated/exactly captured items with `basis: explicit`, allocate stable kind ordinals, and produce normal LSN/change-log entries. - The transcript retains the source structured exchange; graph readers expose the committed nodes/edges; the live web observer updates after capture. - Capture failures are loud and diagnosable (`structural_illegal`, policy/authority result, or explicit no-capture), not silent partial writes. - **Verification:** Inner — capture classification fixtures; command-input shape tests; no-bypass tests. Middle — replay a structured-exchange response fixture through capture and assert graph/change-log/projection results; negative fixtures for low-confidence material and malformed responses. Outer — manual/probe run: user answers a structured prompt, capture commits a small graph slice, web observer updates. - **Topology materialization:** `session/` owns transcript/exchange extraction; `graph/capture/` owns capture-to-command translation and structural/domain policy; `.pi/extensions/structured-exchange` remains an adapter; `.pi/extensions/graph` remains a tool adapter; `rpc/` and `web/` observe through projection handlers only. -- **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. -- **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 / I30-L, I31-L / A22-L, A3-L. +- **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. ### graph-tool-resilience -- **Name:** Broaden direct graph-tool proof beyond the A14 happy path +- **Name:** Materialize graph write contract and broaden direct graph-tool proof - **Linear:** [FE-808](https://linear.app/hash/issue/FE-808/broaden-direct-graph-tool-proof-beyond-the-a14-happy-path) - **Branch:** to create — `ln/fe-808-graph-tool-resilience` -- **Kind:** hardening / tracer bullet +- **Kind:** structural hardening / tracer bullet - **Status:** next -- **Objective:** Extend the real `read_graph`/`commit_graph` product-path proof to cover representative failure and complexity cases: existing-node references, structural-illegal diagnostics with bounded retry, and an ambiguity/no-overcommit case. -- **Why now / unlocks:** The A14 commitGraph subclaim is partially validated by one successful run. The POC needs confidence that the direct-commit path is not a handcrafted probe artifact. +- **Certainty:** proving +- **Stabilizes:** I34-L, I39-L, I40-L, I41-L — graph writes need stable node handles, correct approval basis, and supersession acyclicity before capture/review frontiers build on them. +- **Lights up:** real `read_graph` / `commit_graph` path with projected existing-node references, diagnostics/retry, and no-overcommit behavior through the default Brunch runtime factory. +- **Objective:** Materialize the locked graph write contract in schema, domain types, CommandExecutor validation, tool adapters, and snapshots, then extend the real `read_graph`/`commit_graph` product-path proof to representative failure and complexity cases. +- **Why now / unlocks:** The A14 commitGraph subclaim is partially validated by one successful run, but the canonical graph contract has moved: projected node codes, `basis: explicit | implicit`, per-kind ordinal allocation, and supersession acyclicity are now structural invariants. Capture and review-cycle work should not land against the old raw-id / `accepted_review_set` model. - **Acceptance:** + - DB/domain schema stores `kind_ordinal`, allocates it monotonically per `(spec_id, plane, kind)` through `CommandExecutor` counter rows or equivalent, and rejects duplicate `(spec_id, plane, kind, kind_ordinal)` tuples. + - Graph node metadata owns globally unique 1–3 letter presentation labels plus non-exclusive readiness-band membership; snapshots/prompts/tools render projected codes without storing code strings. + - Accepted nodes/edges use only `basis: explicit | implicit`; `propose-graph` direct commits are `implicit`, exact user/reviewed writes are `explicit`, and retired `accepted_review_set` values are rejected. + - `commitGraph` accepts one approval basis for the batch, returns created ids/kind ordinals, resolves existing-node references from projected codes through adapters, and no longer requires agents to use raw DB ids. + - Supersession edge creation validates acyclicity against existing same-spec supersession edges plus proposed batch edges, including intra-batch and mixed cycles. + - Graph-truth vs active-context reads are explicit enough that active-context snapshots do not return dangling edges to hidden superseded nodes. - At least three additional probe scenarios land under `.fixtures/runs/`: existing-node reference, illegal edge/category/stance with retry, and ambiguous prompt where the agent should avoid overcommitting or ask/emit no-op diagnostics according to strategy guidance. - Probe reports record attempts, retry count, diagnostics seen, final graph counts/LSN, and friction. - Tool guidance and `structural_illegal` diagnostics are sufficient for at least one corrected retry path; if not, the report names the gap. - Existing-node refs target the selected spec's graph only. -- **Verification:** Inner — tool schema/adapter tests if guidance changes. Middle/Outer — real model probe runs with transcript/report artifacts; no artificial injection of the module under test that bypasses the default Brunch runtime factory. +- **Verification:** Inner — schema/domain/CommandExecutor tests for ordinal allocation, basis enum rejection, existing-code resolution, supersession acyclicity, active-context filtering, and tool adapter schema/results. Middle/Outer — real model probe runs with transcript/report artifacts; no artificial injection of the module under test that bypasses the default Brunch runtime factory. - **Topology materialization:** Keep probes in `src/probes/` and `.fixtures/runs/`; keep tool adapter code in `src/.pi/extensions/graph/`; keep validators/diagnostics in `src/graph/`; no probe-only graph runtime wiring that product launch does not use. -- **Cross-cutting obligations:** Avoid harness-as-false-proof: the probe must exercise the same default Brunch runtime factory and registered tools that the product uses. Record fitness, not just pass/fail. -- **Traceability:** D4-L, D20-L, D51-L, D53-L / I34-L, I35-L / A14-L, A5-L. +- **Cross-cutting obligations:** Avoid harness-as-false-proof: the probe must exercise the same default Brunch runtime factory and registered tools that the product uses. Record fitness, not just pass/fail. Preserve D62-L/D63-L/D64-L as graph-wide contracts rather than adapter-local conveniences. +- **Traceability:** D4-L, D20-L, D51-L, D53-L, D60-L, D62-L, D63-L, D64-L / I34-L, I35-L, I39-L, I40-L, I41-L / A14-L, A5-L. - **Design docs:** `docs/architecture/probes-and-transcripts.md`; `docs/design/GRAPH_MODEL.md`. +- **Current execution pointer:** `memory/cards/graph-tool-resilience--graph-write-contract.md` scopes the graph write contract materialization chain; build this before capture/review frontiers. ### project-graph-review-cycle @@ -151,18 +142,21 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple - **Branch:** to create — `ln/fe-809-project-graph-review-cycle` - **Kind:** structural / bounded feature - **Status:** next +- **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. - **Objective:** Wire the `project-graph` strategy from real agent proposal generation through `present_review_set` / `request_review`, dry-run gating, approve/request-changes/reject response handling, and atomic `acceptReviewSet` commit. - **Why now / unlocks:** This is the P1 proposal/review story. It is only P0 if the POC demo requires user-reviewed batch graph commitments rather than direct `propose-graph` and capture paths. - **Acceptance:** - The agent can generate a review-set payload with required lens, epistemic status, and grounding/support metadata. - Only dry-run-valid proposals surface as reviewable; invalid generations remain internal to retry/regeneration. - - Approve commits the entire batch through one `CommandExecutor` call, one LSN, one change-log entry; partial acceptance is not representable. + - Approve commits the entire batch through one `CommandExecutor` call, one LSN, one change-log entry, and `basis: explicit`; partial acceptance is not representable. - Request-changes and reject are transcript-visible outcomes; request-changes can trigger a successor proposal or an explicit deferred path. - Web/TUI can observe the proposal/decision state enough for the POC; full review UX polish may remain thin. - **Verification:** Inner — review-set schema tests, dry-run/real-run differential tests, accept atomicity tests. Middle — structured-exchange review-cycle fixture; no-bypass checks. Outer — targeted probe: `project-graph` proposes, user approves, graph updates and web observer sees it. - **Topology materialization:** Review payload schemas/renderers live under `.pi/extensions/structured-exchange` or `.pi/extensions/graph` only as adapter surfaces; proposal validation/translation lives in `graph/` review modules; agent strategy resource lives in `agents/strategies/project-graph.md`; web observes via RPC projections. -- **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. -- **Traceability:** R21, R23 / D4-L, D20-L, D26-L, D27-L, D51-L, D53-L / I11-L, I34-L / A14-L, A16-L. +- **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. ### minimal-authority-shell @@ -172,6 +166,8 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple - **Branch:** to create — `ln/fe-810-minimal-authority-shell` - **Kind:** hardening - **Status:** next +- **Certainty:** proving +- **Stabilizes:** D20-L/D40-L command-result and elicit-mode authority seams for the current POC graph/session paths. - **Objective:** Fill only the authority behavior required for a credible POC: graph writes keep returning structured command results, `elicit` suppresses obvious side-effecting tools, and headless/RPC paths surface structured `needs_human` where the POC actually reaches human-only actions. - **Why now / unlocks:** Full M6 can remain horizon, but the POC must not look unsafe or mode-specific when graph/capture/review paths are exercised. - **Acceptance:** @@ -192,6 +188,9 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple - **Branch:** to create — `ln/fe-811-poc-live-ship-gate` - **Kind:** hardening / release gate - **Status:** next +- **Certainty:** proving +- **Lights up:** fresh-cwd composed product path across TUI, web observer, runtime posture, structured exchange, and graph write surfaces. +- **Stabilizes:** harness-as-false-proof guard for I22-L, I35-L, I38-L, I39-L, I40-L. - **Objective:** Create and pass the final POC runbook that exercises the real entrypoints together: fresh cwd, multi-spec selection, TUI session, web observer, runtime switch, structured exchange, capture/commit, graph update, and probe artifacts. - **Why now / unlocks:** This is the harness-as-false-proof guard. If a test path had to inject modules the product never wires, the POC is not shipped. - **Acceptance:** @@ -204,7 +203,7 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple - **Verification:** Middle/Outer — executable where practical, manual where TUI/browser interaction is unavoidable. Pair every visual assertion with a durable artifact or projection query when possible. - **Topology materialization:** Runbook/probe code lives in `src/probes/` and `.fixtures/runs/`; it must launch product entrypoints rather than import private modules to fake the product path. - **Cross-cutting obligations:** Keep the gate small and real. Do not turn it into a generic e2e framework or use it to backfill unrelated polish. -- **Traceability:** R4, R7, R10, R11, R12, R16, R19, R24, R28 / D5-L, D11-L, D19-L, D21-L, D33-L, D36-L, D52-L, D61-L / I22-L, I32-L, I35-L, I38-L / A5-L. +- **Traceability:** R4, R7, R10, R11, R12, R16, R19, R24, R28 / D5-L, D11-L, D19-L, D21-L, D33-L, D36-L, D52-L, D61-L, D62-L, D63-L, D64-L / I22-L, I32-L, I35-L, I38-L, I39-L, I40-L / A5-L. - **Design docs:** `docs/architecture/probes-and-transcripts.md`; `docs/architecture/pi-ui-extension-patterns.md`; `memory/SPEC.md` verification stance. ### probes-and-transcripts-evolution @@ -238,10 +237,10 @@ The multi-spec workspace model is now explicit: a workspace is the cwd; multiple ## Recently Completed +- 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-02 `agent-graph-integration` enabling slices — Done inside FE-785: runtime vocabulary fixed; source moved from `src/tui-client/.pi` to `src/.pi`; real `read_graph`/`commit_graph` Pi tools route through `CommandExecutor`; default Brunch runtime factory registers graph tools; A14 `propose-graph → commitGraph` probe persisted 4 nodes + 4 edges on first attempt; review-set dry-run gate validates/filters proposal payloads. Verified: targeted tests, `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`, and `npm run verify`. Watch: broad FE-785 bucket is now split into delivery frontiers above. - 2026-06-02 `spec-persistence-and-startup` — Done: specs are DB rows with integer ids and `readiness_grade`; `createSpec` / `getSpec` / `updateReadinessGrade` route through `CommandExecutor` with change-log audit; startup scaffolds `.brunch/workspace.json` + `.brunch/data.db`; session binding collapsed to `{schemaVersion,specId}` and is fork-portable; inventory resolves spec names from DB. Verified: `npm run verify` and real `brunch --mode print` against a fresh cwd. Watch: richer multi-spec initiative/claim model remains deferred by D61-L. -- 2026-06-01 `graph-data-plane` (FE-741) — Done: Drizzle schema/init, graph clock seed, `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `commitGraph` atomic batch mutation, graph snapshot readers, and reconciliation-need substrate. Verified: `npm run verify`. Watch: graph is now real but must be surfaced by `live-graph-observer` and exercised by capture/review frontiers. Older history (including `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` @@ -249,10 +248,8 @@ Older history (including `sealed-pi-profile-runtime-state`, `pi-ui-extension-pat ```text nodes: - live-graph-observer [done · P0] lights up TUI-writer/web-observer graph visibility - agents-composition-layer [next · P0] makes runtime goal/strategy/lens behavior real + graph-tool-resilience [next · P0] materializes graph write contract and broadens A14 proof capture-response-to-graph [next · P0] structured answer -> graph truth -> observer update - graph-tool-resilience [next · P0] broadens A14 direct graph tool proof project-graph-review-cycle [next · 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 @@ -260,11 +257,8 @@ nodes: topology-readmes-and-boundaries [parallel] attach-to-frontier topology hardening edges: - live-graph-observer -[hard]-> capture-response-to-graph - live-graph-observer -[hard]-> poc-live-ship-gate - agents-composition-layer -[hard]-> capture-response-to-graph - agents-composition-layer -[hard]-> graph-tool-resilience - agents-composition-layer -[hard]-> project-graph-review-cycle + graph-tool-resilience -[hard]-> capture-response-to-graph + graph-tool-resilience -[hard]-> project-graph-review-cycle capture-response-to-graph -[hard]-> poc-live-ship-gate graph-tool-resilience -[hard]-> poc-live-ship-gate project-graph-review-cycle -[optional]-> poc-live-ship-gate @@ -285,6 +279,7 @@ horizon: geolog-and-petri-execution notes: + - Completed prerequisites: `agents-composition-layer` supplies runtime prompt/resource posture, and `live-graph-observer` supplies the read-only web observer path expected by `capture-response-to-graph` and `poc-live-ship-gate`. - `project-graph-review-cycle` is P1 unless the POC demo narrative requires batch proposal/review as a central story; promote it to P0 if so. - `topology-readmes-and-boundaries` is not a license for abstract cleanup; it rides with concrete delivery seams. - Multi-spec workspace discipline applies throughout: target the selected/current spec explicitly; no workspace-global graph truth in the POC. diff --git a/memory/SPEC.md b/memory/SPEC.md index f27c1b4f..f52172c1 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -74,7 +74,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c 16. Brunch must keep sessions elicitation-first and offer-first: at idle, the user is responding to a system/assistant-originated elicitation prompt or structured offer rather than initiating ambient free chat. 17. Brunch must support action, radio (single-select), checkbox (multi-select), freeform, and freeform-plus-choice response surfaces as typed transcript-backed interactions. Every option-selection structured exchange must allow an optional user-authored `comment` as additional context separate from custom/Other answers. Multi-question/questionnaire surfaces are deferred; when a complex shape is needed before a bespoke UI lands, Brunch may use just-in-time schema-tagged JSON over `ctx.ui.editor` or an equivalent product relay. In TUI mode a pending response request may replace the default input surface with custom UI; in RPC/probe/web-relay contexts the same semantic interaction may travel through Brunch product handlers or Pi's supported extension UI dialogs. Brunch must be able to project session exchanges from Pi JSONL for post-exchange capture, including registered structured-exchange tool results whose `toolResult.details` is the self-contained structured response payload. -18. Brunch must support `#`-mentions of graph entities anchored to stable IDs, with session-scoped staleness tracking that produces discretionary re-read hints during `prepareNextTurn`. +18. Brunch must support `#`-mentions of graph entities anchored to stable human reference codes (for example `#R3`), resolved internally to graph node IDs, with session-scoped staleness tracking that produces discretionary re-read hints during `prepareNextTurn`. 19. Brunch must enforce a workspace state hierarchy `workspace(cwd) → spec → session`, where the workspace is only the current working directory invocation root, the user explicitly picks or creates one spec within that workspace before any agent loop runs, and then picks or creates a session within that spec. Spec selection persists across `/new`, and each session binds to exactly one spec. 20. Brunch must support multiple elicitation lenses within the `elicitor` agent role, with the agent owning lens selection and offer through transcript-native structured exchanges; lens metadata is carried on elicitor-emitted structured-exchange payload facets for downstream routing. 21. Brunch must distinguish single-exchange elicitation flows from batch-proposal/review-set flows by capture and commitment mechanism: single-exchange answers are captured synchronously by the elicitor at turn boundaries, while batch proposals carry structured entity-draft payloads and are committed only through review-set approval. @@ -104,7 +104,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | A4-L | A monotonic global LSN per commit (one-LSN-per-transaction) is adequate for change-log replay, reconciliation-need ordering, and mention staleness without per-row vector clocks. | high | open | I1-L, I4-L | M4 + M7: replay fidelity and `worldUpdate` ordering tests. | | A5-L | Agent-as-user probes over the public Brunch RPC surface can produce regression-quality transcript artifacts without depending on a parallel brief-library subsystem. | medium | partially validated | D5-L, D48-L, D49-L | FE-744 public-RPC parity proves the deterministic transport/projection substrate for current structured-exchange permutations; future brief-based or generative golden-fixture work must enter through the probe/transcript artifact path. | | A6-L | The graph-native vocabulary can be deferred from explicit per-plane namespacing (`intent.*`, `oracle.*`, etc.) and start unified under `graph.*` without painful rework later. | medium | open | D3-L | M4–M5: if intent-plane plus oracle-plane stubs both fit under one namespace cleanly, the assumption holds. | -| A7-L | ~~`framing_as` as an orthogonal modality on existing node kinds is sufficient for product-intent ontology.~~ | — | **retired** | D7-L, D54-L, D56-L | Validated and retired by Phase 2 node lock: `framing_as` is absorbed by first-class `thesis`, `term`, and `constraint.subtype` kinds plus `goal`. The modality, allowed matrix (I7-L), and "promote on relation-policy pressure" escape hatch are all retired. See [`docs/design/GRAPH_MODEL.md` §framing_as — retired](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#framing_as--retired). | +| A7-L | ~~`framing_as` as an orthogonal modality on existing node kinds is sufficient for product-intent ontology.~~ | — | **retired** | D7-L, D54-L, D56-L | Validated and retired by Phase 2 node lock: `framing_as` is absorbed by first-class `thesis`, `term`, and `constraint` kinds plus `goal`. The modality, allowed matrix (I7-L), and "promote on relation-policy pressure" escape hatch are all retired. See [`docs/design/GRAPH_MODEL.md` §framing_as — retired](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#framing_as--retired). | | A8-L | One reconciliation-need substrate, sharing the same global LSN as the change log, can absorb impasses, conflicts, gaps, and process debt without needing finer kind subtypes in the POC. | medium | open | D8-L | M8 + adversarial fixtures ("contradictory requirements") exercise the substrate; subtype split deferred per Open Question #10. | | A9-L | A session-scoped mention ledger of (`entity_id`, `snapshotted_lsn`) is the right granularity for staleness hints; transcript-scoped or graph-scoped ledgers are not needed for the POC. | low | open | I7-L | M7 — turn-boundary reconciliation slice; observed via fixture runs that stress re-read decisions. | | A11-L | Pi's `prepareNextTurn` plus custom-message delivery are sufficient to express side-task result delivery without inventing a second event plane or forking pi. | medium | open | D15-L | M5 + M7: side-task registry wiring and next-turn delivery proof. | @@ -134,22 +134,25 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c #### Data model & vocabulary - **D3-L — Graph-native, session-native vocabulary; no generic `records.*` surface.** Commands converge on `graph.*` / `session.*` (with per-plane families `intent.*`, `oracle.*`, `design.*`, `plan.*` available when sharper semantics are useful). Depends on: A6-L. Supersedes: —. -- **D7-L — ~~`framing_as` modality, not first-class kinds.~~ Retired.** `framing_as` is absorbed by first-class `thesis`, `term`, `constraint.subtype`, and `goal` kinds per the Phase 2 node lock. No node carries a `framing_as` field. See [`docs/design/GRAPH_MODEL.md` §framing_as — retired](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#framing_as--retired). Depends on: A7-L (retired). Superseded by: D54-L, D56-L. +- **D7-L — ~~`framing_as` modality, not first-class kinds.~~ Retired.** `framing_as` is absorbed by first-class `thesis`, `term`, `constraint`, and `goal` kinds per the Phase 2 node lock. No node carries a `framing_as` field. See [`docs/design/GRAPH_MODEL.md` §framing_as — retired](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#framing_as--retired). Depends on: A7-L (retired). Superseded by: D54-L, D56-L. - **D8-L — Reconciliation needs are a first-class substrate alongside graph truth, change log, and a bounded coherence verdict.** Needs (impasses, gaps, contradictions, process debt) share the same global LSN as the change log and follow the same mutation invariant. Per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#reconciliationneed--separate-substrate-not-a-graph-edge), each need targets exactly one of `{kind: 'edge', edgeId}` or `{kind: 'node_pair', aId, bId}` and is not itself a graph edge. For the POC, coherence is not an unbounded aesthetic or philosophical judgment; it is the product-visible verdict produced from structural legality plus surfaced contradictions/gaps/unresolved needs, with the exact rubric still open under A21-L until M8. Depends on: A8-L, A21-L. Refined by: D51-L. Supersedes: any `concerns`-edge wiring from reconciliation needs to graph nodes. - **D9-L — Reasoning records split by shape.** `decision` is graph-native; `impasse` is a reconciliation need, not a graph node; `justification` stays compact (rendered text on the decision) until forced otherwise. Phase 2 (per `docs/design/GRAPH_MODEL.md`) keeps `decision` as a plain node rather than a hyper-edge / hub-node for the POC. Depends on: D8-L. Supersedes: —. -- **D54-L — Graph node shape is a common flat interface with `title`, `body`, `basis`, `source`, and a per-kind `detail` JSON column; canonical contract is [`docs/design/GRAPH_MODEL.md` §GraphNode](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#graphnode--the-single-shape).** All planes and kinds share one `nodes` table. `plane` determines which closed `kind` enum applies; `kind` is structurally validated. `basis ∈ explicit | accepted_review_set` (same semantics as edges). `source` is a free-form string for epistemic attribution (e.g. "stakeholder", "regulatory", "derived") — convention by prompt, not structural validation; it exists for context-snapshot enrichment and will be rendered back into sparse text, not used for policy or filtering. `detail` is an optional JSON column with per-kind validated sub-structures: `decision` requires `{ chosen_option, rejected, rationale }`, `term` requires `{ definition, aliases? }`; all other kinds must omit `detail`. `provenance` is retired from the node shape — `change_log` at `createdAtLsn` owns all audit trail. The intent kind rubric (modality of claim + source question per kind) is agent-facing prompting guidance in GRAPH_MODEL.md §"Prompting guidance for kind discrimination", not structural enforcement. Depends on: D4-L, D16-L, D52-L, D56-L. Supersedes: D7-L (`framing_as` modality), the deferred Phase 2 node placeholder in prior GRAPH_MODEL.md. -- **D55-L — `provenance` retired from both edges and nodes; `change_log` owns all audit trail.** Transcript entry pointers (`sessionId`, `entryId`, `proposalEntryId`) are fragile under compaction and redundant with `change_log` + `basis`. `basis` tells you the authority path; `change_log[createdAtLsn]` tells you the durable audit context. Edges retain `basis` and `rationale`. Nodes have `basis` and `source` (epistemic attribution). Depends on: D16-L, D51-L, D54-L. Supersedes: `EdgeProvenance` from Phase 1 edge lock, the planned node-side `provenance` symmetry with edges. -- **D56-L — Intent node kinds: 11 kinds in 3 derived categories (basic / structural / reasoning); canonical contract is [`docs/design/GRAPH_MODEL.md` §Per-plane node kinds](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#per-plane-node-kinds).** `basic` (goal, thesis, term, context) carries grounding material; `structural` (requirement, assumption, constraint, invariant) carries core specification; `reasoning` (decision, criterion, example) carries decisions and evidence. Category is a pure function of `kind` — not stored on the node. The `basic` category maps to spec-grade progression: grounding-gate readiness depends on satisficing threshold of basic-category nodes (D57-L). `thesis` carries "what/who/why/for whom" material (La Carte Blanche style). `term` carries canonical naming commitments (ubiquitous language). `invariant` is first-class (not a constraint subtype) because its operational role differs: invariants get `dependency` and `proof` edges, constraints get `boundary` edges. Each intent kind has a modality-of-claim and source-question rubric for agent prompting (GRAPH_MODEL.md §"Prompting guidance"). Oracle (check, validation_method, evidence, obligation), design (module, interface), and plan (milestone, frontier, slice) kinds are stable from worked examples. Depends on: D54-L. Supersedes: D7-L (`framing_as`), A7-L. -- **D57-L — Spec-grade grounding gate is LLM-judged satisficiency with a count floor on basic-category nodes.** The gate from `grounding_onboarding` toward `elicitation_ready` is not structurally enforced by rubric coverage checks. The agent judges readiness using prompt-embedded abstract drivers (Walter-style: what is it, who is it for, what problem, what value, when used, how measured) but cannot declare grounding complete with zero `basic`-category nodes. Grounding elicitation may establish workspace posture, but posture is not a spec-row field or graph node kind in the POC. Depends on: D45-L, D56-L. Supersedes: D30-L grounding-bundle anchor vocabulary as the sole readiness gate description. Refines: D30-L, D45-L. -- **D51-L — Graph edge model is a closed structural-category set with a separate ReconciliationNeed substrate; canonical contract is [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md).** Every accepted edge is one of eight closed categories (`dependency`, `proof`, `support`, `realization`, `boundary`, `composition`, `association`, `supersession`); `stance: for | against` is valid only on `proof` and `support`; `basis ∈ explicit | accepted_review_set` (no `inferred`). Accepted edges have no mutable `status` field — `proposed` lives in review-set drafts, `rejected` is absent + change-log audit, `stale` is represented by a `ReconciliationNeed`. Identity fields (`category`, `sourceId`, `targetId`, `stance`) are immutable on an accepted edge; a "category change" is delete + recreate. Only `dependency` cascades automatically; other categories surface advisory recon-needs rather than auto-blocking. Cross-plane edges are unrestricted at the POC stage; `realization` subtypes (implementation/establishment/assertion/etc.) may be derived from node-tuple lookup later rather than encoded on the edge. `ReconciliationNeed` is a separate substrate whose target is exactly `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` — it is not itself a graph edge. Depends on: D4-L, D8-L, D16-L, D27-L, A14-L. Supersedes: the named-relation catalogue in `docs/architecture/pi-seam-extensions.md` §"Edge types" (`validates`, `instance_of`, `produces`, `discharges`, `depends_on`, `derived_from`, `counterexample_for`, `witnesses`), the per-relation policy registry / lookup, the brainstormed expanded edge taxonomy in `archive/docs/design/GRAPH_EDGE_CATEGORIES.md`, and any `concerns`-edge wiring from reconciliation needs to graph nodes. +- **D54-L — Graph node shape is a common flat interface with `kind_ordinal`, `title`, `body`, `basis`, `source`, and a per-kind `detail` JSON column; canonical contract is [`docs/design/GRAPH_MODEL.md` §GraphNode](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#graphnode--the-single-shape).** All planes and kinds share one `nodes` table. `id` is the internal SQLite integer/FK identity; `kind_ordinal` is the monotonic per-`(spec, plane, kind)` ordinal used with `kind` to project a stable human reference code (D62-L). The rendered code string is not stored in the database. `plane` determines which closed `kind` enum applies; `kind` is structurally validated. `basis ∈ explicit | implicit` records item-level approval strength per D63-L. `source` is a free-form string for epistemic attribution (e.g. "stakeholder", "regulatory", "derived", "agent synthesis") — convention by prompt, not structural validation; it exists for context-snapshot enrichment and will be rendered back into sparse text, not used for policy or filtering. `detail` is an optional JSON column with per-kind validated sub-structures: `decision` requires `{ chosen_option, rejected, rationale }`, `term` requires `{ definition, aliases? }`; all other kinds must omit `detail`. `provenance` is retired from the node shape — `change_log` at `createdAtLsn` owns the audit trail, while `basis` and `source` carry only local interpretation fields. The intent kind rubric (modality of claim + source question per kind) is agent-facing prompting guidance in GRAPH_MODEL.md §"Prompting guidance for kind discrimination", not structural enforcement. Depends on: D4-L, D16-L, D52-L, D56-L, D62-L, D63-L. Supersedes: D7-L (`framing_as` modality), the deferred Phase 2 node placeholder in prior GRAPH_MODEL.md. +- **D55-L — `provenance` retired from both edges and nodes; `change_log` owns audit trail and mutation path.** Transcript entry pointers (`sessionId`, `entryId`, `proposalEntryId`) are fragile under compaction and redundant with `change_log` keyed by `createdAtLsn` / `updatedAtLsn`. `basis` does **not** encode the transport or strategy path; per D63-L it records whether the exact graph item was user-approved (`explicit`) or agent-materialized after concept-level approval (`implicit`). `change_log.operation` and payload record the durable mutation context (`create_node`, `commit_graph`, `accept_review_set`, etc.). Edges retain `basis` and `rationale`; nodes retain `basis` and `source` (epistemic attribution). Depends on: D16-L, D51-L, D54-L, D63-L. Supersedes: `EdgeProvenance` from Phase 1 edge lock, the planned node-side `provenance` symmetry with edges, and the former `accepted_review_set` basis-as-path enum. +- **D56-L — Intent node kinds: 11 kinds in 3 derived semantic categories (basic / structural / reasoning); canonical contract is [`docs/design/GRAPH_MODEL.md` §Per-plane node kinds](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#per-plane-node-kinds).** `basic` (goal, thesis, term, context) carries grounding material; `structural` (requirement, assumption, constraint, invariant) carries core specification; `reasoning` (decision, criterion, example) carries decisions and evidence. This category is a pure function of intent `kind` — not stored on the node — and remains distinct from the cross-plane readiness-band metadata in D64-L. `thesis` carries "what/who/why/for whom" material (La Carte Blanche style). `term` carries canonical naming commitments (ubiquitous language). `invariant` is first-class (not a constraint subtype) because its operational role differs: invariants get `dependency` and `proof` edges, constraints get `boundary` edges. Each intent kind has a modality-of-claim and source-question rubric for agent prompting (GRAPH_MODEL.md §"Prompting guidance"). Oracle (check, validation_method, evidence, obligation), design (module, interface), and plan (milestone, frontier, slice) kinds are stable from worked examples and receive prefix/readiness-band metadata through D62-L/D64-L. Depends on: D54-L, D62-L, D64-L. Supersedes: D7-L (`framing_as`), A7-L. +- **D57-L — Spec-grade grounding gate is LLM-judged satisficiency over readiness-band evidence with a count floor, not a hard kind whitelist.** The gate from `grounding_onboarding` toward `elicitation_ready` is not structurally enforced by rubric coverage checks. The agent judges readiness using prompt-embedded abstract drivers (Walter-style: what is it, who is it for, what problem, what value, when used, how measured) plus D64-L readiness-band evidence. The grounding threshold centers on grounding-band nodes such as `goal`, `thesis`, `term`, and `context`, and may also count grounding-relevant constraints because a constraint anchor can be part of the frame. The agent cannot declare grounding complete with zero grounding-band graph evidence, but obvious lower-grade `requirement`, `criterion`, `check`, or design nodes may still be captured when the user clearly gives them; those nodes simply do not by themselves prove the grounding threshold. Grounding elicitation may establish workspace posture, but posture is not a spec-row field or graph node kind in the POC. Depends on: D45-L, D56-L, D64-L. Supersedes: D30-L grounding-bundle anchor vocabulary as the sole readiness gate description. Refines: D30-L, D45-L. +- **D51-L — Graph edge model is a closed structural-category set with a separate ReconciliationNeed substrate; canonical contract is [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md).** Every accepted edge is one of eight closed categories (`dependency`, `proof`, `support`, `realization`, `boundary`, `composition`, `association`, `supersession`); `stance: for | against` is valid only on `proof` and `support`; `basis ∈ explicit | implicit` follows D63-L (no `inferred`, no `accepted_review_set` path value). Accepted edges have no mutable `status` field — `proposed` lives in review-set drafts, `rejected` is absent + change-log audit, `stale` is represented by a `ReconciliationNeed`. Identity fields (`category`, `sourceId`, `targetId`, `stance`) are immutable on an accepted edge; a "category change" is delete + recreate. `supersession` chains are acyclic and the `CommandExecutor` must validate acyclicity against existing same-spec edges plus proposed batch edges. Only `dependency` cascades automatically; other categories surface advisory recon-needs rather than auto-blocking. Cross-plane edges are unrestricted at the POC stage; `realization` subtypes (implementation/establishment/assertion/etc.) may be derived from node-tuple lookup later rather than encoded on the edge. `ReconciliationNeed` is a separate substrate whose target is exactly `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` — it is not itself a graph edge. Depends on: D4-L, D8-L, D16-L, D27-L, A14-L, D63-L. Supersedes: the named-relation catalogue in `docs/architecture/pi-seam-extensions.md` §"Edge types" (`validates`, `instance_of`, `produces`, `discharges`, `depends_on`, `derived_from`, `counterexample_for`, `witnesses`), the per-relation policy registry / lookup, the brainstormed expanded edge taxonomy in `archive/docs/design/GRAPH_EDGE_CATEGORIES.md`, any `concerns`-edge wiring from reconciliation needs to graph nodes, and the former `accepted_review_set` edge-basis value. - **D61-L — A spec is an initiative answering a problem; its truth-bearing units are claims resolved at node level.** A spec's identity is its problem-answering initiative, not the product areas, seams, or domains it touches; it may reach a done-state while those keep evolving. Its truth-bearing units ("claims") are the existing `structural` and `reasoning` intent node kinds (requirement, assumption, constraint, invariant, decision, criterion, example) under D54-L/D56-L — `claim` is a vocabulary umbrella, not a new node kind — so revision, conflict, and supersession resolve at node level (supersession edges per D51-L), not at whole-spec level. POC scope: each spec owns its own intent graph (no cross-spec claim sharing); the `workspace → spec → session` hierarchy (D11-L) is unchanged and `spec.readiness_grade` (D45-L) remains the only persisted spec-state — no initiative-status column is added. The full initiative/claim model (cross-spec claim survival/adoption, initiative-status lifecycle, spec-to-spec relationships, current-truth-as-projection) is deferred to Future Direction §Spec initiative & claim model; rationale: [`docs/design/SPEC_INITIATIVE_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/SPEC_INITIATIVE_MODEL.md). Depends on: D11-L, D45-L, D54-L, D56-L. Supersedes: —. +- **D62-L — Graph nodes have stable spec-scoped human reference codes projected from stored `kind_ordinal`, separate from integer storage IDs.** `NodeId` remains the SQLite integer primary key/FK used internally. The database stores `kind` and `kind_ordinal`; user/agent-facing handles such as `G1`, `CON2`, `R3`, `CR4`, `VM1`, or `SL2` are projection strings formed by a hard-coded presentation lookup from `kind` to a 1–3 capital-letter label plus `kind_ordinal`. The rendered code string is not a graph column. Labels are unique across all node kinds so `#`-mentions can parse by longest-prefix match, then resolve to `(kind, kind_ordinal)` and finally to `NodeId`. `kind_ordinal` is monotonic per `(spec_id, plane, kind)`, allocated by the `CommandExecutor` in the same transaction as node creation from a counter row (`node_kind_counters` or equivalent), not by `MAX(kind_ordinal)+1`; ordinals are never reused after deletion or supersession. DB constraints must make `(spec_id, plane, kind, kind_ordinal)` unique; there is no `(spec_id, code)` uniqueness constraint because `code` is not stored. Snapshots and prompt contexts should render projected codes as primary handles and reserve raw integer IDs for internal diagnostics/adapters. Depends on: D14-L, D16-L, D20-L, D54-L, D56-L, D61-L. Supersedes: the string-`NodeId` examples in earlier GRAPH_MODEL text and the previous app's application-only `MAX(kind_ordinal)+1` allocation pattern. +- **D63-L — Graph `basis` records item-level approval strength, not the mutation pathway.** Accepted nodes and edges use `basis ∈ explicit | implicit`. `explicit` means the user directly stated the graph item or approved the exact node/edge in a review set; `implicit` means the user accepted a concept/proposal and the agent materialized specific graph items to match it without per-item review (the `propose-graph` direct-commit path). The mutation pathway lives in `change_log.operation` and payload (`commit_graph`, `accept_review_set`, post-exchange capture, etc.), while epistemic attribution lives in `Node.source` and proposal UI metadata may still carry `epistemic_status`. Low-confidence inferred material is still not graph truth; it remains in preface/capture analysis/review drafts/reconciliation needs until clarified or accepted. Depends on: D26-L, D27-L, D53-L, D54-L, D55-L. Supersedes: `basis = accepted_review_set` as a persisted graph enum value and any interpretation of `basis` as a provenance/path field. +- **D64-L — Readiness bands are non-exclusive derived node-kind groupings used for elicitor goals, snapshots, and grade rubrics; they are not structural legality gates.** Bands are `grounding`, `elicitation`, and `commitment`. A node kind may belong to multiple bands (for example `constraint` can contribute to grounding when it is the constraint anchor and to elicitation when it bounds solution space). Bands guide what the elicitor is trying to complete at a given `readiness_grade`, what graph filters/snapshots can show, and what evidence a readiness validator considers. The `CommandExecutor` must not reject a clear `requirement`, `criterion`, `check`, design node, or other later-band kind merely because the spec is at an earlier grade; readiness controls objectives and unlocks, not what graph truth may contain. Depends on: D45-L, D56-L, D57-L, D59-L, D60-L. Supersedes: treating the intent `basic | structural | reasoning` category as the readiness taxonomy or treating readiness as a per-kind creation whitelist. #### Authority & mutation - **D4-L — One shared mutation surface owns graph truth.** Every semantic graph mutation routes through Brunch-owned typed command handlers responsible for validation, structural legality, optimistic concurrency, event emission, audit attribution, and coherence triggering. Agents and adapters must not touch the ORM or SQLite directly. Depends on: A3-L. Supersedes: —. - **D20-L — Command execution owns the pre-M6 authority seam.** Callers submit product commands to a Brunch `CommandExecutor` and receive a structured result; they do not call a standalone authority service or graph persistence directly. The executor is the public mutation boundary that hides attribution, optimistic concurrency, structural validation, the minimal pre-M6 policy classifier, transaction execution, LSN allocation, change-log append, and coherence-trigger hooks. Before M6, the policy logic may be deliberately small, but the result shape must already include `needs_human`, `policy_blocked`, `version_conflict`, and `structural_illegal` so early RPC, print, agent-tool, deferred observer/auditor, and side-task code cannot bake in permissive mode-specific shortcuts. Depends on: D4-L, D16-L. Supersedes: the separate optional `AuthorityGate` / generic policy-service mental model. -- **D27-L — Review-set proposals are structured entity-draft payloads; batch acceptance is one atomic `CommandExecutor` call.** The elicitor's review-set proposal is carried inside a structured-exchange `present_review_set` / `request_review` flow rather than as a standalone `brunch.review_set_proposal` transcript-entry family. Its payload contains the graph entities and edges that *would* be created on acceptance — edge drafts follow the locked contract in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (`{category, sourceId, targetId, stance?, basis: 'accepted_review_set'}`) rather than a free-form `relation` string — in a form `CommandExecutor` can dry-run-validate at proposal time so `structural_illegal` / `policy_blocked` discriminants surface before the user reviews. Only proposals that pass this dry-run validation are surfaced as user-reviewable review sets; invalid generations stay internal to retry/regeneration paths rather than becoming review UI state. Acceptance is one `acceptReviewSet` command that consumes one LSN, writes the entire batch in one transaction, appends one change-log entry attributed to the user, triggers coherence updates, and enqueues any reviewer job. "Accept with edits" does not exist as a primitive: the cycle is approve / request changes (triggers regeneration of a successor proposal) / reject. Applies to batch-proposal flows and commitment review sets. Depends on: A14-L, D4-L, D20-L, D26-L. Supersedes: any caller-side multi-step "patch then commit" mental model or standalone review-set-proposal custom-entry contract. -- **D53-L — `commitGraph` is a single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references.** The propose-graph strategy's load-bearing tool for direct graph commitment after concept-level user acceptance (D26-L). The agent produces one tool call with a `nodes` array (each carrying a temporary batch `ref`, `kind`, and content fields) and an `edges` array (each carrying `category`, source/target as either an intra-batch `ref` or an existing node id, optional `stance`, and `rationale`). The CommandExecutor validates all nodes structurally, assigns real NodeIds to each batch ref, resolves intra-batch and existing-node references, validates all edges per the closed category set and structural invariants in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), allocates one LSN, writes all nodes + edges + change-log in one SQLite transaction, and returns success with created ids or `structural_illegal` with diagnostics sufficient for agent self-correction. On validation failure the agent may retry within a bounded budget; the user does not see intermediate failures. `commitGraph` and `acceptReviewSet` (D27-L) are parallel paths to the same CommandExecutor — one for direct agent-authored commits after concept acceptance, one for user-reviewed batch proposals. Depends on: D4-L, D20-L, D51-L, D52-L. Supersedes: —. +- **D27-L — Review-set proposals are structured entity-draft payloads; batch acceptance is one atomic `CommandExecutor` call.** The elicitor's review-set proposal is carried inside a structured-exchange `present_review_set` / `request_review` flow rather than as a standalone `brunch.review_set_proposal` transcript-entry family. Its payload contains the graph entities and edges that *would* be created on acceptance — edge drafts follow the locked category contract in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (`{category, source, target, stance?, rationale?}` over draft ids / projected existing node codes) rather than a free-form `relation` string — in a form `CommandExecutor` can dry-run-validate at proposal time so `structural_illegal` / `policy_blocked` discriminants surface before the user reviews. Only proposals that pass this dry-run validation are surfaced as user-reviewable review sets; invalid generations stay internal to retry/regeneration paths rather than becoming review UI state. Acceptance is one `acceptReviewSet` command that consumes one LSN, writes the entire batch in one transaction, appends one change-log entry attributed to the user, triggers coherence updates, enqueues any reviewer job, and writes accepted nodes/edges with `basis: explicit` because the user approved the exact reviewed items (D63-L). "Accept with edits" does not exist as a primitive: the cycle is approve / request changes (triggers regeneration of a successor proposal) / reject. Applies to batch-proposal flows and commitment review sets. Depends on: A14-L, D4-L, D20-L, D26-L, D63-L. Supersedes: any caller-side multi-step "patch then commit" mental model or standalone review-set-proposal custom-entry contract. +- **D53-L — `commitGraph` is a single-tool atomic batch mutation accepting one approval basis plus `{ nodes, edges }` with intra-batch and existing-node references.** The propose-graph strategy's load-bearing tool for direct graph commitment after concept-level user acceptance (D26-L) uses `basis: implicit` per D63-L; review-set acceptance and synchronous high-confidence capture use explicit-basis command paths when the exact items were user-stated or user-reviewed. The agent produces one tool call with a `nodes` array (each carrying a temporary batch `ref`, `plane`, `kind`, and content fields) and an `edges` array (each carrying `category`, source/target as either an intra-batch `ref` or an existing-node reference that tool adapters may accept as a projected code and resolve to `(kind, kind_ordinal)`, optional `stance`, and `rationale`). The `CommandExecutor` validates all nodes structurally, allocates kind ordinals per D62-L, resolves intra-batch and existing-node references to internal integer NodeIds, validates all edges per the closed category set and structural invariants in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) including supersession acyclicity, allocates one LSN, writes all nodes + edges + change-log in one SQLite transaction, and returns success with created ids/kind ordinals (adapters may render projected codes) or `structural_illegal` with diagnostics sufficient for agent self-correction. On validation failure the agent may retry within a bounded budget; the user does not see intermediate failures. `commitGraph` and `acceptReviewSet` (D27-L) are parallel paths to the same CommandExecutor — one for direct agent-authored commits after concept acceptance, one for user-reviewed batch proposals. Depends on: D4-L, D20-L, D51-L, D52-L, D62-L, D63-L. Supersedes: command inputs that expose per-item `accepted_review_set` basis values or require agents to refer to existing nodes by raw DB ids. #### Transport & client @@ -226,9 +229,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D37-L — Structured elicitation is Pi-transcript-native; structured exchanges use durable toolResult families.** A system/assistant-originated structured interaction should use the thinnest Pi-supported transcript seam for its shape. The preferred Brunch seam is registered Pi tool results: `present_*` tools persist and display assistant-originated offer/question/proposal material, `request_*` tools collect and persist the user response, and future `capture_*` tools persist assistant analysis of candidate semantic changes without mutating graph truth. The assistant `toolCall` supplies call identity and arguments, but durable semantic display is the `toolResult` row rendered by that tool's `renderResult`; `renderCall` is transient header/progress only and must not carry Brunch semantic display. `toolResult.content` is rich markdown that is both transcript display content and model-readable context; `toolResult.details` is the structured projection/recovery payload. The landed Zod-authored target details model under `src/.pi/extensions/structured-exchange/schemas/` uses checked `schema` + `v` discriminants, `exchange_id`, compact `tool_meta` sequence/sibling metadata, exactly-one request outcome presence (`answered` | `cancelled` | `unavailable`), user-authored `comment` versus runtime-authored `message`, strict `present_candidates` rubrics/`graph_refs`, and intentionally minimal no-graph `capture_*` details. Runtime tools/projections still use the existing tuple details model until a deliberate migration slice rewires them to these exports. Implemented present/request tools use `executionMode: "sequential"`; FE-744's real Pi RPC ordering proof validates that same-assistant-message `present_options → request_choice` persists the present `toolResult` before the request `toolResult` and emits the present `tool_execution_end` before the request UI opens, and the public Brunch RPC parity proof drives the current deterministic tuple-shaped permutation set over product methods only. RPC event consumers should not assume `request_*` `tool_execution_start` precedes its extension UI request, because Pi may emit the UI request first. Standalone Brunch custom entries remain valid for genuinely non-exchange session facts such as `brunch.session_binding`, `brunch.agent_runtime_state`, lens switches, side-task results, and mention/world-update delivery; they are not the default carrier for establishment offers, review-set proposals, intent hints, or structured response surfaces. RPC/web paths answer the same semantic pending interaction through Brunch product handlers or Pi-supported dialog fallbacks rather than depending on TUI-only `ctx.ui.custom()`. Depends on: D12-L, D13-L, D17-L, D19-L, D38-L, D41-L. Supersedes: treating all structured offers as Brunch custom entries, treating render lifecycle state as durable transcript state, relying on ephemeral dialog results detached from transcript truth, modeling a structured exchange as one split-brain tool row whose present half lives in `renderCall`, or treating the retired scope-card contract as canonical after the schema README and tests have landed. - **D38-L — JSON-over-editor is the Pi-RPC compatibility seam for complex extension UI, not a second product API.** Pi RPC supports `ctx.ui.select`, `confirm`, `input`, and `editor`, but not `ctx.ui.custom()`. When a structured-exchange tool needs a complex shape (multi-select, review-style response, or a deferred multi-question/questionnaire shape) over raw Pi RPC, the tool may call `ctx.ui.editor()` with schema-tagged JSON prefill and validate the returned JSON before producing normal `toolResult.content` plus self-contained `toolResult.details`. A Brunch-aware adapter may render that JSON as a native product form and translate the user response back into Pi's documented `extension_ui_response`; public clients still speak Brunch RPC methods/events, not ad hoc raw Pi RPC extensions. Depends on: D5-L, D19-L, D33-L, D37-L. Supersedes: inventing unsupported Pi RPC command types for Brunch interactions or exposing raw editor JSON as the product UX. - **D13-L — Capture-aware session exchange projection.** Post-exchange capture consumes derived session exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text and/or terminal structured-exchange `request_*` toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. -- **D14-L — `#`-mentions are stable-handle text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable handle (`#A12`, `#I7`, or equivalent node handle) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret the handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. Per-session `(entity_id, snapshotted_lsn)` ledger drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata. +- **D14-L — `#`-mentions are stable graph-code text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable graph node code from D62-L (`#G1`, `#CON2`, `#R3`, `#CR4`, etc.) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret these handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. The ledger stores internal `(entity_id, snapshotted_lsn)` pairs, not titles or raw code strings alone, and drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, D62-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata or using raw DB ids as user-facing handles. - **D25-L — Strategy and lens are two orthogonal session-agent axes within the `elicitor` role, not separate roles or operational modes.** *Strategies* describe interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`); *lenses* describe topical focus (`intent`, `design`, `oracle`; future execute-mode `plan`, `sync`, `scope`). Both are optional, AUTO-able fields of the projected session-agent record (D40-L) and are stamped onto structured-exchange payload facets (for example establishment offers, intent hints, and review/proposal material) when those facets need downstream routing; capture/reviewer/audit routing may filter on lens. Strategy determines the commitment mechanism (D26-L); the catalogue is expected to grow. Depends on: D23-L, D40-L. Supersedes: lens-as-role, strategy-as-mode, and standalone elicitor-intent/establishment/review custom-entry families as the default carrier. -- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation; commitment (`commit-converge` goal and active review-set state, D59-L) changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. +- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L; graph items directly stated by the user are written with `basis: explicit`. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L); accepted exact items are written with `basis: explicit`. (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape — so those materialized nodes/edges are written with `basis: implicit` (D63-L). Design/oracle lenses may appear during ordinary elicitation; commitment (`commit-converge` goal and active review-set state, D59-L) changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L, D63-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. - **D30-L — Grounding advances readiness for main elicitation; strategies remain available with honest epistemic signaling.** A minimum grounding bundle — *domain anchor*, *protagonist anchor*, *pain/pull anchor*, *constraint anchor* — establishes the frame required to move the spec from `grounding_onboarding` toward `elicitation_ready`. Lenses and strategies are not refused merely because grounding is thin, but their output resolution and epistemic load must honestly reflect what grounding supports: speculative outputs are visibly hedged and lower-authority, while grounded outputs may drive capture and later review-set projection. Grounding coverage should be explicit in offers/proposals where it affects confidence or gate transitions. Depends on: D26-L, D45-L. Supersedes: gating-by-refusal as a UX move and over-focusing readiness on generative lenses alone. - **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** Establishment-offer material records the agent's current offer tree and recommended next move as durable structured-exchange payload state when it is part of an exchange, not as a mandatory standalone transcript entry family. Ambient chrome or web affordances may render the latest establishment-offer facet, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu or separate transcript store. - **D31-L — A four-axis meta-rubric is a soft heuristic for fan-out comparison rubrics across all three flows; not architecturally enforced.** When generating comparison rubrics for fan-out alternatives across candidate-spec, technical-design, and verification-design flows, the elicitor attempts to express each axis in terms of (*legibility / cost-of-knowing*, *failure modes*, *coverage / range*, *commitment*). Project-specific axes are allowed alongside; the meta-frame is dropped when it doesn't fit. The hypothesis (uniform comparison UI across all three flows is more useful than per-flow improvisation) is testable via fixture comparison; promote to schema/UI only if it holds up. Depends on: D25-L, D26-L. Supersedes: a hardcoded per-flow rubric. @@ -244,7 +247,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. - **D58-L — Brunch prompt composition is a thin runtime header plus a gated prompt-resource manifest, not eager selection of every objective pack.** `agents/compose(agentId, sessionState, spec, workspace, snapshots)` runs before Pi provider requests through Brunch's prompt extension and emits: **(1) agent control header** — keyed agent identity, model/thinking expectation, foreground role derived from `op_mode`, and mode/tool-authority summary; **(2) runtime-state header** — current pinned/AUTO `goal`, `strategy`, and `lens`, `spec.readiness_grade`, and workspace posture; **(3) resource manifests** — XML-style ``, ``, ``, and `` entries filtered by `agents/state.ts` legal tuples, grade, `op_mode`, and the agent allow-list, each carrying `{name, description, location}` for a Brunch-owned markdown resource under `src/agents/`; the `{name, description, location}` triples are code-owned in `agents/state.ts`, not filesystem-discovered, honoring D39-L sealing; **(4) compact pushed context** — only the minimal snapshot summary/handles needed to orient the turn, with detailed snapshot content still governed by D60-L. Detailed goal/strategy/lens/method instructions live in Brunch prompt resources and are loaded by the agent with `read` when needed, following the same simple mechanism Pi uses for skills. Method resources are the prompt-level home for Brunch tool-routing and sequencing guidance; tool definitions remain boundary schemas/execution hooks, not the whole Brunch guide to when or how tools should be composed. `AUTO` means the axis is unpinned: the manifest lists legal choices and router instructions tell the agent to choose only from the current manifest, reading the selected resource before applying it when detail matters. Pinned axes point to the pinned resource; code enforces legality and tool gating but does not choose or concatenate large semantic packs on the agent's behalf. Pi-native skills may still carry startup-scoped capabilities, but runtime-state-gated availability is Brunch's manifest, not ambient Pi discovery. `agents/` is a keyed resource registry (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods/`, `contexts/`); `agents/contexts/` is the D60-L snapshot render layer (code), not a manifest resource family; composition is projection, not a behavioral state machine. Depends on: D23-L, D25-L, D39-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; "role preset / runtime bundle" as the composition unit; direct Layer-2 eager prompt-pack injection as the default mechanism; and `capability` as a parallel name for `method` / ``. - **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-expand` (expand the elicited specification graph while ambiguity remains productive), `commit-converge` (reduce / lock down reviewable commitments), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO`; in either case D58-L manifests advertise the legal resource(s) rather than injecting the whole goal body. `elicit-expand` and `commit-converge` intentionally form the diverge/converge pair for the elicitation diamond; `elicit-I` / `elicit-II` are retired because they were phase-like labels, not objectives. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. -- **D60-L — "Snapshot" splits into pull / render / surface, and names two distinct subjects.** **Agent-context snapshot** = content the agent reasons over: `cwd` (filesystem kickoff heuristic — `.brunch?`, session count/length, README/markdown sizes, file counts), `graph` (overview), `node` (variable-hop neighborhood). **PULL** is typed, read-only data access owned by the data layer (`graph/snapshot.ts` for graph/node; `session/` for cwd) and bypasses `CommandExecutor` (reads only); the typed value *is* the JSON form. **RENDER** turns the typed value into either an LLM-friendly string (owned solely by `agents/contexts/`, scaled by lens-plane and grade-depth) or JSON (trivial serialization). **SURFACE** delivers it: *pushed* (compose injects at turn boundary), *pulled* (thin `snapshot-*` Pi tools wrap the renderer — markdown in `toolResult.content`, typed JSON in `toolResult.details` per I33-L), or *rpc/ui*. The separate **workspace projection** (`workspace.snapshot` — workspace/session/spec/chrome product state) is a different subject and keeps that name; reserve "snapshot" for the agent-context family. Depends on: D35-L, D52-L, D53-L. Supersedes: pre-rendering snapshots to strings in the pull layer, and scattering snapshot build logic across `graph/`, `agents/contexts/`, and the `snapshot-*` tool stubs. +- **D60-L — "Snapshot" splits into pull / render / surface, distinguishes graph-truth from active-context projections, and names two distinct subjects.** **Agent-context snapshot** = content the agent reasons over: `cwd` (filesystem kickoff heuristic — `.brunch?`, session count/length, README/markdown sizes, file counts), `graph` (overview/list/query), or `node` (variable-hop neighborhood). **PULL** is typed, read-only data access owned by the data layer (`graph/snapshot.ts` for graph/node; `session/` for cwd) and bypasses `CommandExecutor` (reads only); the typed value *is* the JSON form. Graph pulls must make the projection explicit: `graph_truth` includes accepted truth records, while `active_context` hides superseded predecessors and must also omit edges whose endpoints are hidden so snapshots do not contain dangling references. The graph read family should support the observed query shapes without becoming a generic records API: list nodes by kind(s), list nodes by D64-L readiness band(s), and find nodes related to anchor node(s) by edge category/direction/hop depth. **RENDER** turns the typed value into either an LLM-friendly string (owned solely by `agents/contexts/`, scaled by lens-plane and grade-depth) or JSON (trivial serialization), rendering projected stable node codes (D62-L) as primary handles. **SURFACE** delivers it: *pushed* (compose injects at turn boundary), *pulled* (thin `snapshot-*` / graph-read Pi tools wrap the renderer — markdown in `toolResult.content`, typed JSON in `toolResult.details` per I33-L), or *rpc/ui*. The separate **workspace projection** (`workspace.snapshot` — workspace/session/spec/chrome product state) is a different subject and keeps that name; reserve "snapshot" for the agent-context family. Depends on: D35-L, D52-L, D53-L, D62-L, D64-L. Supersedes: pre-rendering snapshots to strings in the pull layer, scattering snapshot build logic across `graph/`, `agents/contexts/`, and the `snapshot-*` tool stubs, or silently mixing graph-truth and active-context reads. ### Critical Invariants @@ -280,14 +283,17 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | 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 | -| I31-L | `readiness_grade` is a forward gate, not a workflow location: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable; all grade mutations route through `CommandExecutor` and carry audit through the change log. | partial (Card 1: specs table plus `createSpec` / `getSpec` / `updateReadinessGrade` command tests; M5 prompt/tool-policy tests for grade-gated availability remain) | D20-L, D45-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 | | I34-L | `commitGraph` batch validation is all-or-nothing: if any node or edge in the batch is structurally illegal, the entire batch is rejected and no partial state is persisted; the agent receives diagnostics sufficient for bounded self-correction retry. | covered (22 tests in `command-executor.test.ts` — edge failure rolls back nodes, mixed-batch rejection, diagnostic sufficiency) | D53-L; I1-L, I11-L | -| I35-L | Graph context snapshots support multiple detail levels: a cursory/compact full-graph overview for orientation, and detailed node-neighborhood snapshots with configurable hop depth for focused work. Context builders in `agents/contexts/` orchestrate which level to inject or advertise based on mode/goal/strategy/lens/grade. | partially covered (`getGraphOverview` + `getNodeNeighborhood` in `snapshot.ts` with 10 tests; context-builder integration deferred to M5) | D52-L, D53-L, D58-L | +| I35-L | Graph context snapshots support multiple detail levels: a cursory/compact full-graph overview for orientation, and detailed node-neighborhood snapshots with configurable hop depth for focused work. Context builders in `agents/contexts/` orchestrate which level to inject or advertise based on mode/goal/strategy/lens/grade. | covered for current POC push path (`getGraphOverview` + `getNodeNeighborhood` in `snapshot.ts` with 10 tests; `src/agents/contexts/{graph,node,cwd}.test.ts` covers lens-shaped overview rendering, bounded node-neighborhood rendering, and selected-spec cwd/session/posture context; `src/.pi/__tests__/prompting.test.ts` proves the explicit shell/product prompt path supplies selected-spec-bound graph snapshots to `composeAgentPrompt()`). Pulled `snapshot-*` tools remain optional future surface. | D52-L, D53-L, D58-L | | I36-L | Node `kind` is drawn from a per-plane closed enum structurally validated by the `CommandExecutor`; the intent kind category (basic / structural / reasoning) is a pure function of `kind` and is never stored on the node. | covered (CommandExecutor rejects invalid kind-for-plane; `intentKindCategory` is pure derivation with exhaustive switch; tests in `command-executor.test.ts`) | D54-L, D56-L | | I37-L | `detail` is per-kind validated by the `CommandExecutor`: `decision` and `term` nodes REQUIRE `detail` with their respective sub-schemas; all other kinds must omit `detail`; unknown fields in `detail` are rejected. | covered (detail-required/prohibited/shape tests in `command-executor.test.ts`) | D54-L | -| 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. | planned (`agents-composition-layer` compose tests for manifest filtering, read locations, tuple/grade/allow-list gates, and ambient-resource exclusion; probe fitness may track whether the agent reads selected resources before use) | D39-L, D40-L, D58-L, D59-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. | planned (`graph-tool-resilience` CommandExecutor tests for counter allocation, batch allocation, rollback, deletion/supersession no-reuse, and projected-code lookup; DB unique constraint on stored ordinal tuple) | 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`. | planned (`agent-turn-to-graph-capture`, `graph-tool-resilience`, and `project-graph-review-cycle` adapter/CommandExecutor tests for basis assignment and schema rejection of retired basis values) | 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. | planned (`graph-tool-resilience` CommandExecutor tests for existing-cycle, intra-batch-cycle, mixed existing+batch-cycle, and rollback behavior) | D51-L, D53-L; I34-L | ## Future Direction Register @@ -311,7 +317,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is a **runtime-header + gated prompt-resource manifest** composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshots)` (D58-L). The direct injection is intentionally small: agent control summary, runtime state, legal resource manifests (``, ``, ``, `` with `name`, `description`, `location`), router rules for pinned/AUTO axes, and compact context handles. Detailed goal/strategy/lens/method bodies are Brunch-owned markdown resources the agent loads with `read` when needed, matching Pi's skill-loading pattern while letting Brunch filter availability per turn. The current `src/.pi/context/` layout migrates into `src/agents/`. +- Brunch prompt composition is a **runtime-header + gated prompt-resource manifest** composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshots)` (D58-L). The direct injection is intentionally small: agent control summary, runtime state, legal resource manifests (``, ``, ``, `` with `name`, `description`, `location`), router rules for pinned/AUTO axes, and compact context handles/rendered snapshots. Detailed goal/strategy/lens/method bodies are Brunch-owned markdown resources the agent loads with `read` when needed, matching Pi's skill-loading pattern while letting Brunch filter availability per turn. The old `src/.pi/context/` prompt-pack layout is retired; product prompt assets live under `src/agents/`. - Concrete `agents/` topology (D52-L). The markdown/code boundary falls exactly on the control-plane/behavior split: enforcement and projection are TypeScript; semantic prompting material is markdown. ```text @@ -333,7 +339,7 @@ src/agents/ - Manifest metadata is code-owned, not filesystem-discovered: `agents/state.ts` binds each legal axis value to its `{name, description, location}`, and `compose()` emits that binding; the agent `read`s the `.md` body at the listed `location` only when detail matters. This keeps the legal set and its labels in one tested place and honors D39-L sealing (no runtime resource discovery). Frontmatter-sourced manifest metadata is a deferred ergonomics option, not the POC mechanism. - `agents/contexts/` is the D60-L snapshot render layer (TypeScript), surfaced as the header's compact pushed context or via the `snapshot-*` tools; it is not part of the `read`-on-demand resource manifest and carries no `` family. - Workspace **posture** is workspace-scoped product state persisted in `.brunch/workspace.json`, not spec state, session state, or graph truth. D57-L keeps it off the spec row and graph; D58-L composition injects known posture values into the runtime header as an axis of agent influence, and the `capture-posture` goal (D59-L) can confirm or refine those values conversationally. -- Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` lives on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. +- Readiness is an internal forward gate, not a user-facing workflow stepper, session-local phase, or graph-node-kind whitelist. `readiness_grade` lives on the spec row per D45-L; D64-L readiness bands describe non-exclusive evidence/rubric groupings for goal selection and snapshot filtering. Validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. - Prompt resources and Pi skills are both progressive-disclosure mechanisms, but they are not authority. Brunch code owns runtime-state projection, legal tuple filtering, grade/allow-list gating, tool activation, and tool-call blocking. Pi-native skills may be used for startup-scoped capabilities; runtime-state-specific objective/method availability is advertised through Brunch's per-turn manifest so ambient user/project resources cannot leak into product behavior. ### Coherence and readiness semantics @@ -344,7 +350,7 @@ src/agents/ ### Vocabulary evolution - Whether public graph commands eventually split from one `graph.*` umbrella into `intent.*` / `oracle.*` / `design.*` / `plan.*` namespaces is deferred; current posture is unified `graph.*` for the POC. -- ~~Whether `framing_as` values graduate to first-class node kinds~~ — resolved: `framing_as` retired, absorbed by `thesis`, `term`, `constraint.subtype`, and `goal` (D54-L, D56-L). +- ~~Whether `framing_as` values graduate to first-class node kinds~~ — resolved: `framing_as` retired, absorbed by `thesis`, `term`, `constraint`, and `goal` (D54-L, D56-L). - `posture` is a workspace-level POC-stubbed property set for now; whether it earns richer persistence or graph-native representation is deferred until product pressure shows concrete readers beyond startup/prompt context. ### Thin transport/read posture @@ -399,12 +405,12 @@ src/agents/ | **Prompt resource** | A Brunch-owned markdown file under `src/agents/` containing detailed goal, strategy, lens, method, or agent-definition guidance. Prompt resources are loaded by the agent with `read` when needed; they are product control-plane assets, not ambient Pi prompt templates. | | **Prompt-resource manifest** | The small per-turn D58-L manifest injected into the system prompt, listing only runtime-legal Brunch resources with `name`, `description`, and `location`. The `name`/`description`/`location` for each entry are code-owned in `agents/state.ts` (not filesystem-discovered), honoring D39-L sealing; `agents/contexts/` snapshot renderers are not manifest resources. It mirrors Pi's skill-list pattern but is filtered by Brunch runtime state, grade, and allow-lists. | | **Method** | A tool-usage or workflow competence advertised as a Brunch prompt resource (`agents/methods/*.md`): run structured exchanges, infer-and-capture (D50-L), generate proposals/projections, read snapshots, mutate the graph, review for gaps. Method resources explain when to use a tool family and how to sequence it with other tools; executable tool definitions should stay focused on schemas, authority, and runtime behavior. A method may also be backed by a Pi-native skill, but actual tool authority remains code-owned through `op_mode` policy and active-tool gating. `capability` is retired as a synonym — use `method` and ``. | -| **Snapshot** | An *agent-context* content view the agent reasons over — `cwd`, `graph`, or `node` (D60-L): pulled (typed, read-only) from `graph/`/`session/`, rendered to LLM-string (in `agents/contexts/`) or JSON, surfaced pushed (compose) or pulled (`snapshot-*` tools). Distinct from the **workspace projection** (`workspace.snapshot`), which is product/UI state, not agent content. | -| **Readiness grade** | Spec-owned forward gate stored on the `specs` row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | +| **Snapshot** | An *agent-context* content view the agent reasons over — `cwd`, `graph`, or `node` (D60-L): pulled (typed, read-only) from `graph/`/`session/`, rendered to LLM-string (in `agents/contexts/`) or JSON, surfaced pushed (compose) or pulled (`snapshot-*` / graph-read tools). Graph snapshots explicitly choose graph-truth vs active-context projection and may filter by node kind, readiness band, or edge category/direction. Distinct from the **workspace projection** (`workspace.snapshot`), which is product/UI state, not agent content. | +| **Readiness grade** | Spec-owned forward gate stored on the `specs` row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, review sets, and eventual export/plan/execute posture, but never forbids earlier gathering, refinement, or capture of clear later-band node kinds. | | **Elicitation posture** | Retired as persisted spec state. Use readiness grade plus active strategy/lens/review-set state to explain elicit behavior. | | **Commitment focus** | Retired as persisted spec state. Future commitment projection should derive from active review-set state and graph evidence if needed. | | **Coherence** | Bounded product-visible verdict over whether the current spec graph is structurally legal and free of known unresolved contradictions/gaps at the current maturity. It is backed by reconciliation needs and remains intentionally narrower than a general judgment that the whole idea is good or complete. | -| **Structural legality** | Synchronous schema/ontology validity of graph mutations: edge categories from the closed set in `docs/design/GRAPH_MODEL.md`, per-category stance/cardinality/acyclicity rules, immutable accepted-edge identity (`category`, `sourceId`, `targetId`, `stance`), per-plane closed node `kind` enums, required `detail` sub-schemas for `decision`/`term`, `constraint.subtype` enum, and transaction invariants. Structural legality can fail even before semantic coherence is evaluated. | +| **Structural legality** | Synchronous schema/ontology validity of graph mutations: edge categories from the closed set in `docs/design/GRAPH_MODEL.md`, per-category stance/cardinality/acyclicity rules (including supersession cycles), immutable accepted-edge identity (`category`, `sourceId`, `targetId`, `stance`), per-plane closed node `kind` enums, stable kind-ordinal uniqueness/counter allocation, approval-basis enum validity, required `detail` sub-schemas for `decision`/`term`, and transaction invariants. Structural legality can fail even before semantic coherence is evaluated. | | **Print snapshot** | The M1 meaning of the print transport mode: boot the Brunch host, resolve workspace/spec/session state through the coordinator, render product-shaped state, and exit without running an agent turn. | | **Workspace** | The current working directory where the Brunch CLI was invoked. It scopes `.brunch/` state for the launch context. It is not user-created, not selectable within the dialog, and there is only one active workspace per Brunch process. The UI may display a project identity/name derived from cwd-local manifests or directory basename, but that name labels the cwd; it does not create a separate workspace object. | | **Spec / specification** | A user-created **initiative that exists to answer a problem** well enough to guide coordinated work, and that can reach a done-state even though the product, domains, and architecture keep evolving (D61-L). Concretely it is a container within a workspace, identified by its intent-graph root, holding sessions and the truth-bearing graph data (claims) gathered through them; the areas, seams, and domains it touches are not its identity. Multiple specs may coexist under one workspace; future plan-execution mode operates on a selected spec. | @@ -421,13 +427,14 @@ src/agents/ | **Oracle graph** | Verification-strategy plane accountable to intent. Houses Checks, Validation Methods, Evidence, Obligations. | | **Design graph** | Modules, interfaces, seams, and adapters accountable to intent. Stubbed in POC. | | **Plan graph** | Milestone/frontier/slice delivery claims accountable to intent, oracle, and design. Stubbed in POC. | +| **Graph node code** | Stable spec-scoped human handle projected for a graph node from a hard-coded kind label plus stored monotonic per-kind ordinal (for example `G1`, `CON2`, `R3`, `CR4`). The code string is not stored in graph tables; internal lookup resolves it to `kind` + `kind_ordinal` and then to integer `NodeId`. Primary handle for `#`-mentions, snapshots, and agent prompts (D62-L). | | **LSN** | Log Sequence Number. A single monotonic counter, one-LSN-per-commit, shared by the change log, graph-node versions, and reconciliation needs. | | **Change log** | The audit trail of graph mutations. Authoritative for replay, `worldUpdate` synthesis, and reconciliation-need ordering. | | **Reconciliation need** | First-class record of an open impasse, gap, contradiction, or process debt; carries `created_at_lsn`, optional `resolved_at_lsn`, and a target that is exactly one of `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` per `docs/design/GRAPH_MODEL.md`. Recon-needs are a separate substrate, not graph edges (no `concerns`-edge wiring). Routine async jobs are not reconciliation needs unless they surface semantic work to resolve. | | **Coherence verdict** | Per-spec product state (`coherent` / `incoherent`) emitted by validators and visible to both UI and agent. | | **Command layer** | The single Brunch-owned mutation surface. Validates, gates concurrency, audits, emits events, triggers coherence. Its public mutation entry point is the `CommandExecutor`, not direct ORM calls or caller-side authority gates. | -| **Command executor** | The deep module that accepts Brunch product commands plus execution context and returns structured command results (`ok`, `needs_human`, `policy_blocked`, `version_conflict`, `structural_illegal`). It hides attribution, minimal pre-M6 authority classification, validation, transaction, LSN, change-log, and coherence-trigger mechanics from callers. | -| **commitGraph** | Single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references. One tool call, one LSN, all-or-nothing (I34-L). The load-bearing tool for the `propose-graph` strategy's direct-commit path (D53-L). | +| **Command executor** | The deep module that accepts Brunch product commands plus execution context and returns structured command results (`ok`, `needs_human`, `policy_blocked`, `version_conflict`, `structural_illegal`). It hides attribution, minimal pre-M6 authority classification, validation, kind-ordinal allocation, transaction, LSN, change-log, and coherence-trigger mechanics from callers. | +| **commitGraph** | Single-tool atomic batch mutation accepting one approval basis plus `{ nodes, edges }` with intra-batch and existing-node references. One tool call, one LSN, stable kind-ordinal allocation, all-or-nothing (I34-L). The load-bearing tool for the `propose-graph` strategy's direct-commit path (D53-L), where concept-level materialization is `basis: implicit` (D63-L). | | **propose-graph** | Elicitor strategy for generative lenses where the agent proposes a novel coherent subgraph. The concept is presented to the user with rubric axes, choices, and recommendation via structured exchange; upon acceptance the agent generates and persists the full subgraph through `commitGraph` without intermediate entity-level review (D26-L, D53-L). The hardest thing to get structurally legal and the primary proof target for A14-L. | | **project-graph** | Elicitor strategy for deriving nodes and edges from existing graph truth (e.g. projecting requirements from upstream goals/constraints). Uses review-set commitment (D27-L). Extractive rather than inventive; lower structural-legality risk than propose-graph. | | **Brunch public RPC surface** | The one product-facing JSON-RPC surface exposed over stdio, WebSocket, and in-process handlers. Product clients use this surface for workspace, session, graph, and coherence projections plus session-native interaction methods; raw Pi RPC is hidden behind adapters when needed. | @@ -470,13 +477,15 @@ src/agents/ | **Mention ledger** | Per-session `(entity_id, snapshotted_lsn)` record driving discretionary staleness hints when an entity has changed since the agent last saw it. | | **Authority** | Source of a node's claim: `stakeholder | technical | external | derived`. | | **Epistemic status** | Confidence basis: `observed | asserted | assumed | inferred`. Like `authority`, this is a context-shaping label for attention, grouping, and compression rather than a complete theory of truth. | -| **Framing-as** | ~~Orthogonal modality classifying a node's product role.~~ **Retired.** Absorbed by `thesis`, `term`, `constraint.subtype`, and `goal` (D54-L, D56-L). | +| **Framing-as** | ~~Orthogonal modality classifying a node's product role.~~ **Retired.** Absorbed by `thesis`, `term`, `constraint`, and `goal` (D54-L, D56-L). | | **Thesis** | A first-class intent node kind (`kind: "thesis"`). A chosen position or bet about the product — falsifiable, carries "what/who/why/for whom" material (La Carte Blanche style). Not a requirement (it's a bet, not a need), not a goal (it's falsifiable, not aspirational), not an assumption (it's a chosen position, not a dependency). Natural edge relationships: criteria and evidence witness for/against a thesis via `proof` edges. | | **Term** | A first-class intent node kind (`kind: "term"`). A canonical naming commitment for ubiquitous language and conceptual consistency. Requires `detail: { definition, aliases? }`. Participates in graph edges: downstream nodes may `dependency`-depend on the term's definition; a term may `boundary`-scope what counts as X; a newer term may `supersession`-replace a prior term. | -| **Node source** | Free-form string on `GraphNode.source` for epistemic attribution (e.g. "stakeholder", "regulatory", "derived"). Convention by prompt, not structural validation. Exists for context-snapshot enrichment — rendered back into sparse text in prompt snapshots, not used for policy or filtering. Not applicable to edges. | -| **Node detail** | Optional JSON column on `GraphNode.detail` with per-kind validated sub-structures. `decision` requires `{ chosen_option, rejected, rationale }`; `term` requires `{ definition, aliases? }`; `constraint` may carry `{ subtype }`. All other kinds omit `detail`. | +| **Graph basis** | Approval-strength field on accepted graph nodes and edges: `explicit` when the exact item was directly stated or user-reviewed; `implicit` when the agent materialized specific items after concept-level acceptance. Mutation path lives in `change_log`, not in `basis` (D63-L). | +| **Node source** | Free-form string on `GraphNode.source` for epistemic attribution (e.g. "stakeholder", "regulatory", "derived", "agent synthesis"). Convention by prompt, not structural validation. Exists for context-snapshot enrichment — rendered back into sparse text in prompt snapshots, not used for policy or filtering. Not applicable to edges. | +| **Node detail** | Optional JSON column on `GraphNode.detail` with per-kind validated sub-structures. `decision` requires `{ chosen_option, rejected, rationale }`; `term` requires `{ definition, aliases? }`. All other kinds omit `detail`. | | **Context (node kind)** | A first-class intent node kind (`kind: "context"`). A descriptive claim about the environment — observed facts that color interpretation without driving decisions directly. Last-resort basic bucket: before filing as context, check the promotion heuristic (must be true for success → requirement/invariant; limits solutions → constraint; may be false → assumption; chooses among alternatives → decision; bet about users/market → thesis). | -| **Intent kind category** | Derived grouping of intent node kinds: `basic` (goal, thesis, term, context), `structural` (requirement, assumption, constraint, invariant), `reasoning` (decision, criterion, example). A pure function of `kind`, not stored. Maps to spec-grade progression — grounding gate requires satisficing threshold of basic-category nodes. | +| **Intent kind category** | Derived semantic grouping of intent node kinds: `basic` (goal, thesis, term, context), `structural` (requirement, assumption, constraint, invariant), `reasoning` (decision, criterion, example). A pure function of `kind`, not stored. Distinct from readiness bands. | +| **Readiness band** | Non-exclusive derived grouping over node kinds — `grounding`, `elicitation`, `commitment` — used by elicitor goals, graph snapshot filters, and grade-advancement rubrics. A band is not a validation gate; clear later-band nodes may be captured at earlier grades (D64-L). | | **Posture** | A workspace-level POC-stubbed property set declaring project epistemic/strategic stance (certainty, stakes, audience, horizon, migration, sourcing). Not a graph node kind or spec-row field in the POC. Grounding elicitation may help establish it, but startup persists only the workspace stub. | | **Kernel** | A behavioural elicitation pattern from `docs/design/BEHAVIORAL_KERNELS.md` (state/lifecycle, containment, concurrency, etc.). | | **Probe run** | A scripted or executable check of a Brunch seam that drives the public product surface and persists reviewable artifacts under `.fixtures/runs///`. | @@ -555,10 +564,10 @@ Infrastructure is not yet fully laid (Phase 3 of POC bootstrapping). Commands fo | Loop | Oracle family | Proves | Primary claims | | --- | --- | --- | --- | | Inner | Type-aware lint, type checks, fast unit tests | Local module correctness, typed command/result shapes (including `acceptReviewSet` and reviewer-writable record-class types), projection helper behavior (including `supersedes`-chain filtering). | D12-L, D13-L, D20-L, D21-L, D27-L, D28-L, D29-L. | -| Inner | Schema/shape validation at boundaries | JSON-RPC payloads, command results, structured elicitation entries, Zod-authored structured-exchange present/request/capture details with JSON Schema export, probe report metadata, graph exports, runtime-gated prompt-resource manifests, and structured-exchange payload facets for review proposals, establishment offers, and elicitor intent hints (lens presence, `epistemic_status`, grounding coverage, entity-draft shape). | R8, R10, R11, R17, R20, R21, R23; I3-L, I10-L, I11-L, I17-L, I18-L, I23-L, I26-L, I38-L. | +| Inner | Schema/shape validation at boundaries | JSON-RPC payloads, command results, structured elicitation entries, Zod-authored structured-exchange present/request/capture details with JSON Schema export, probe report metadata, graph exports, graph node-code/basis fields, runtime-gated prompt-resource manifests, and structured-exchange payload facets for review proposals, establishment offers, and elicitor intent hints (lens presence, `epistemic_status`, grounding coverage, entity-draft shape). | R8, R10, R11, R17, R20, R21, R23; I3-L, I10-L, I11-L, I17-L, I18-L, I23-L, I26-L, I38-L, I39-L, I40-L. | | Middle | **Probe oracles**: prose manual actions plus executable postcondition checkers | Interactive seams leave correct durable state. Early M0 checkers may inspect stores only; once handlers exist, prefer projection-including checks. Extends to workspace-dialog startup behavior, in-flight reviewer-signal chrome behavior, and ambient-affordance rendering from latest establishment-offer structured-exchange facet. | D11-L, D21-L, D22-L, D25-L, D29-L, D36-L; I8-L, I13-L, I22-L. | | Middle | Round-trip tests | JSONL reload, linear transcript validation, session exchange projection, compaction, graph export/import, command result serialization, `supersedes`-chain reconstruction across regeneration. | D6-L, D13-L, D24-L, D28-L; I3-L, I8-L, I10-L, I19-L. | -| Middle | Property-based / model-based tests | LSN monotonicity, change-log replay, reconciliation-need invariants, mention staleness, interest-set recomputation, side-task delivery ordering, **batch-acceptance atomicity (one LSN / one change-log entry, partial-batch impossible even under mid-batch validation failure)**, **`supersedes`-chain acyclicity and unique-leaf-per-thread**, **lens-routing correctness (generated elicitor entries route to the right consumer)**, **reviewer-finding turn-boundary delivery ordering**. | A4-L, A8-L, A9-L, A11-L; I1-L, I4-L, I5-L, I6-L, I9-L, I12-L, I15-L, I16-L, I18-L. | +| Middle | Property-based / model-based tests | LSN monotonicity, change-log replay, reconciliation-need invariants, stable kind-ordinal allocation/no-reuse, mention staleness, interest-set recomputation, side-task delivery ordering, **batch-acceptance atomicity (one LSN / one change-log entry, partial-batch impossible even under mid-batch validation failure)**, **`supersedes` / `supersession` acyclicity and unique-leaf-per-thread**, **lens-routing correctness (generated elicitor entries route to the right consumer)**, **reviewer-finding turn-boundary delivery ordering**. | A4-L, A8-L, A9-L, A11-L; I1-L, I4-L, I5-L, I6-L, I9-L, I12-L, I15-L, I16-L, I18-L, I39-L, I41-L. | | Middle | Contract tests | Named RPC method families and transport adapters share handler semantics; `rpc.discover` describes public methods with usable schemas/examples; `session.triggerExchange` / `session.pendingExchange` / `session.submitExchangeResponse` / `session.exchanges` preserve transcript truth; subscriptions deliver initial snapshot plus ordered updates; `CommandExecutor` hides policy/transaction details; `acceptReviewSet` returns expected structured discriminants; only prevalidated proposals become reviewable review sets. | D5-L, D19-L, D20-L, D27-L, D48-L, D49-L; R11, R12, R27, R28. | | Middle | Architectural boundary tests | No direct ORM/SQLite mutation outside `CommandExecutor`; no canonical chat/turn store; TUI/RPC/fixture code does not write `brunch.session_binding`; spec/session picker UI returns decisions rather than opening/mutating sessions; RPC/headless boot exposes structured initial-selection state instead of invoking TUI picker code; Brunch wrappers do not expose Pi branch creation/navigation as product behavior; spec readiness-grade mutations route through commands rather than session-local memory; reviewer-attributed writes target only `reconciliation_need`; Brunch-launched Pi runtimes do not load ambient `.pi/` resources or behavior-shaping settings outside the Brunch Pi Profile; Brunch product extensions load through the explicit static shell list rather than filesystem discovery or a runtime extension-metadata protocol. | D4-L, D6-L, D18-L, D21-L, D24-L, D29-L, D36-L, D39-L, D45-L; I2-L, I10-L, I11-L, I16-L, I19-L, I22-L, I24-L, I31-L. | | Middle | **Differential testing** | Dry-run validation at proposal time matches real-run validation at acceptance time (no drift between modes); free-form-generation vs constrained-generation legality rates (informs whether fallback path is needed per A14-L). | D27-L; A14-L. | @@ -612,12 +621,15 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | 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. | -| I31-L | Spec-row command tests for grade updates plus prompt/tool-policy tests proving grade gates unlock later actions without disabling gathering/refinement. Card 1 covers the CommandExecutor grade-write path; prompt/tool-policy tests remain with M5. | +| 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. | | I36-L | M4 per-plane kind enum validation tests in CommandExecutor; kind-to-category derivation unit tests proving pure function parity with GRAPH_MODEL.md table. | | I37-L | M4 node-creation tests: decision/term rejected without detail; constraint accepted with or without detail; other kinds rejected with detail; unknown detail fields rejected. | | 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 tests: counter rows allocate monotonic per-kind ordinals in multi-node batches, rollback does not persist failed ordinals, deletion/supersession does not permit ordinal reuse, projected-code lookup resolves through `(kind, kind_ordinal)`, and DB constraints reject duplicate `(spec_id, plane, kind, kind_ordinal)`. | +| I40-L | Adapter/CommandExecutor tests across `agent-turn-to-graph-capture`, `graph-tool-resilience`, and `project-graph-review-cycle`: direct user/accepted review-set writes are `explicit`, `propose-graph` materialization is `implicit`, retired `accepted_review_set` basis values are rejected, and `change_log.operation` preserves the mutation path. | +| I41-L | `graph-tool-resilience` CommandExecutor tests for supersession acyclicity across existing edges, intra-batch edges, and mixed existing+batch edges, including rollback on illegal cycles. | ### Design Notes diff --git a/memory/cards/graph-tool-resilience--graph-write-contract.md b/memory/cards/graph-tool-resilience--graph-write-contract.md new file mode 100644 index 00000000..02bca5b8 --- /dev/null +++ b/memory/cards/graph-tool-resilience--graph-write-contract.md @@ -0,0 +1,353 @@ +# Graph write contract materialization + +Frontier: graph-tool-resilience +Status: active +Mode: chain +Created: 2026-06-04 + +## Orientation + +- Containing seam: `graph-tool-resilience` (FE-808), reshaped from probe-only hardening into the graph write contract materialization frontier. +- Posture: proving (inherited from `graph-tool-resilience`). This slice chain should stabilize I39-L/I40-L/I41-L before capture/review frontiers build on graph writes. +- Main risk: adapters, snapshots, and tests may still assume raw integer node ids and `accepted_review_set` basis values; remove the old shape directly rather than bridging it. +- Cross-cutting obligations: preserve `CommandExecutor` as the only graph mutation authority; keep projected node codes out of DB storage; keep readiness bands as query/rubric metadata, not write-time kind gates. + +## Card 1 — Persist per-kind node ordinals + +Status: next + +### Target Behavior + +Every committed graph node receives a monotonic, non-reused ordinal scoped to `(spec_id, plane, kind)`. + +### Boundary Crossings + +```pseudo +agent/capture/review command input +→ graph/CommandExecutor +→ db/schema.ts nodes + node_kind_counters +→ graph/snapshot row mappers +``` + +### Risks and Assumptions + +- RISK: allocating ordinals inside failed batches may leave gaps. + → MITIGATION: require monotonic/no-reuse, not gaplessness; rollback tests should prove failed batches do not persist nodes or change-log entries. +- ASSUMPTION: a small counter table is cheaper and clearer than deriving ordinals by scanning existing nodes. + → IMPACT IF FALSE: CommandExecutor allocation code changes, but projected-code contract remains. + → VALIDATE: transaction tests around batch allocation, rollback, and duplicate DB constraints. + +### Posture check + +This stabilizes I39-L and proves the storage half of D62-L through the real mutation boundary. + +### Acceptance Criteria + +```pseudo +✓ command-executor.test.ts — multi-node batches allocate kind_ordinal per (spec, plane, kind) +✓ command-executor.test.ts — failed batches do not persist nodes, edges, change-log entries, or unusable counter state +✓ db/schema tests or migration checks — duplicate (spec_id, plane, kind, kind_ordinal) rows are structurally impossible +✓ snapshot.test.ts — GraphNode domain objects expose kindOrdinal +``` + +### Verification Approach + +- Inner: CommandExecutor + DB schema tests — prove allocation, rollback, and uniqueness. +- Middle: none for this card; later cards exercise adapters/product tools. + +### Cross-cutting obligations + +- Do not store rendered reference-code strings in graph tables. +- Keep ordinals spec-scoped; no workspace-global graph truth. + +### Expected touched paths (tentative) + +```pseudo +src/db/ +├── schema.ts ~ +├── row-schemas.ts ~ +└── README.md ~ +drizzle/ +└── *.sql + +src/graph/ +├── command-executor.ts ~ +├── command-executor.test.ts ~ +├── snapshot.ts ~ +├── snapshot.test.ts ~ +├── spec-ownership.test.ts ~ +├── atoms.ts ~ +├── index.ts ~ +└── schema/ + └── nodes.ts ~ +``` + +## Card 2 — Replace path-shaped graph basis with approval basis + +Status: next + +### Target Behavior + +Accepted graph nodes and edges persist only `basis: explicit | implicit`. + +### Boundary Crossings + +```pseudo +propose-graph / capture / review-set adapter basis decision +→ graph/CommandExecutor input +→ db enum constraints +→ snapshots and graph domain types +``` + +### Risks and Assumptions + +- RISK: current tool schemas allow callers to supply per-node/per-edge basis and preserve `accepted_review_set`. + → MITIGATION: move basis to the command/adaptation context; reject retired values in tests. +- ASSUMPTION: mutation path is recoverable enough from `change_log.operation` and payload. + → IMPACT IF FALSE: need richer change-log payloads, not a third basis enum. + → VALIDATE: tests assert both stored basis and change-log operation for each write path. + +### Posture check + +This stabilizes I40-L and removes the most misleading compatibility bridge in the graph model. + +### Acceptance Criteria + +```pseudo +✓ command-executor.test.ts — commitGraph accepts one batch approval basis and applies it to all created nodes/edges +✓ command-executor.test.ts — retired accepted_review_set basis values are rejected at the command boundary or impossible through types/schemas +✓ review-set-proposal tests — accepted review-set translation commits exact reviewed items with explicit basis +✓ graph tool adapter tests — propose-graph commit path supplies implicit basis without asking the agent per item +✓ change-log assertions — mutation operation remains visible independently of basis +``` + +### Verification Approach + +- Inner: domain/schema/adapter tests — prove basis enum and assignment rules. +- Middle: none for this card unless an existing product probe already inspects basis. + +### Cross-cutting obligations + +- `basis` is approval strength only; do not encode strategy or transport path in it. +- Low-confidence inferred material still stays outside graph truth. + +### Expected touched paths (tentative) + +```pseudo +src/db/ +├── schema.ts ~ +└── README.md ~ +src/graph/ +├── command-executor.ts ~ +├── command-executor.test.ts ~ +├── schema/ +│ ├── nodes.ts ~ +│ └── edges.ts ~ +└── index.ts ~ +src/.pi/extensions/graph/ +├── tool-schemas.ts ~ +├── command-adapter.ts ~ +├── review-set-proposal.ts ~ +└── *.test.ts ? +``` + +## Card 3 — Resolve existing graph refs by projected code + +Status: next + +### Target Behavior + +Agent-facing graph write adapters resolve existing-node references from projected codes to internal NodeIds. + +### Boundary Crossings + +```pseudo +commit_graph tool params / review-set payload +→ graph adapter projected-code parser +→ selected-spec graph lookup +→ CommandExecutor NodeId refs +→ tool result rendering +``` + +### Risks and Assumptions + +- RISK: prefix parsing can be ambiguous if labels collide. + → MITIGATION: hard-code globally unique labels and test longest-prefix parsing. +- RISK: CommandExecutor currently owns existing-node spec guards for raw ids. + → MITIGATION: keep selected-spec ownership check after code resolution; adapter resolution must not weaken the guard. +- ASSUMPTION: lower-level CommandExecutor may still use internal NodeId refs after adapter resolution. + → IMPACT IF FALSE: ref-resolution helper moves into graph/ so all adapters share it. + → VALIDATE: adapter and spec-ownership tests. + +### Posture check + +This lights up the D62-L product handle path without making projected code a DB column. + +### Acceptance Criteria + +```pseudo +✓ graph metadata tests — every node kind has a globally unique 1–3 letter label and readiness-band metadata +✓ adapter tests — existingCode values like A1/CON2/CR3 parse to kind + kindOrdinal by longest prefix +✓ adapter/CommandExecutor tests — existingCode resolves only within the selected spec +✓ read_graph formatting tests — success output renders projected codes as primary handles and raw ids only as diagnostics/details when needed +``` + +### Verification Approach + +- Inner: parser/adapter/spec-ownership tests — prove code resolution and selected-spec guard. +- Middle: direct graph tool probe later in the frontier should include an existing-node code reference. + +### Cross-cutting obligations + +- Projected codes are presentation handles; do not store them or make them canonical DB identity. +- Existing refs must target the selected spec only. + +### Expected touched paths (tentative) + +```pseudo +src/graph/ +├── schema/ +│ └── nodes.ts ~ +├── snapshot.ts ~ +├── snapshot.test.ts ~ +├── spec-ownership.test.ts ~ +└── index.ts ~ +src/.pi/extensions/graph/ +├── tool-schemas.ts ~ +├── command-adapter.ts ~ +├── command-adapter.test.ts + +└── index.ts ~ +src/agents/contexts/ +├── graph.ts ~ +├── graph.test.ts ~ +├── node.ts ~ +└── node.test.ts ~ +``` + +## Card 4 — Enforce supersession acyclicity + +Status: next + +### Target Behavior + +Same-spec supersession edge creation rejects every proposed cycle before writing any batch state. + +### Boundary Crossings + +```pseudo +edge command input +→ CommandExecutor structural validation +→ existing same-spec supersession edge read +→ transaction rollback / success result +``` + +### Risks and Assumptions + +- RISK: mixed existing + intra-batch cycles are easy to miss if validation runs only per edge. + → MITIGATION: validate the proposed supersession graph as a set against existing same-spec supersession edges. +- ASSUMPTION: supersession acyclicity is structural legality, not coherence advice. + → IMPACT IF FALSE: downstream active-context projection can hide the wrong current node. + → VALIDATE: cycle tests across existing, intra-batch, and mixed cases. + +### Posture check + +This stabilizes I41-L and removes a documented-but-unenforced graph invariant. + +### Acceptance Criteria + +```pseudo +✓ command-executor.test.ts — rejects simple existing-cycle closure +✓ command-executor.test.ts — rejects intra-batch supersession cycles +✓ command-executor.test.ts — rejects mixed existing+batch supersession cycles +✓ command-executor.test.ts — rejected cycles roll back all nodes/edges/change-log from the batch +✓ command-executor.test.ts — acyclic supersession chains still commit +``` + +### Verification Approach + +- Inner: CommandExecutor structural tests — prove cycle detection and rollback. +- Middle: none required. + +### Cross-cutting obligations + +- Keep cross-plane freedom; acyclicity applies to `supersession` edges, not node-kind legality. + +### Expected touched paths (tentative) + +```pseudo +src/graph/ +├── command-executor.ts ~ +├── command-executor.test.ts ~ +└── policy/ + └── category-policy.ts ? +``` + +## Card 5 — Make active-context graph reads code-aware and non-dangling + +Status: next + +### Target Behavior + +Active-context graph snapshots omit hidden superseded nodes and every edge whose endpoint is hidden. + +### Boundary Crossings + +```pseudo +graph/snapshot pull +→ graph domain projection +→ agents/contexts render +→ read_graph tool result / RPC graph readers +``` + +### Risks and Assumptions + +- RISK: changing `getGraphOverview` semantics could break existing observer UI expectations. + → MITIGATION: make projection choice explicit where the public read surface needs both graph_truth and active_context; default only where current product contract names it. +- ASSUMPTION: active-context filtering belongs in graph pull, while LLM string formatting belongs in agents/contexts or tool adapters. + → IMPACT IF FALSE: projection/rendering responsibilities blur again. + → VALIDATE: snapshot tests plus context-render tests. + +### Posture check + +This stabilizes D60-L and keeps the graph read path aligned with the new code/basis write contract. + +### Acceptance Criteria + +```pseudo +✓ snapshot.test.ts — graph_truth can include superseded predecessors and their edges when requested +✓ snapshot.test.ts — active_context omits superseded nodes and edges touching omitted nodes +✓ agents context tests — rendered graph/node context uses projected codes as primary handles +✓ read_graph adapter tests — details/content remain selected-spec scoped after projection changes +``` + +### Verification Approach + +- Inner: snapshot/context/tool tests — prove projection and rendering split. +- Middle: later frontier probe can exercise browser/read_graph observation after graph writes. + +### Cross-cutting obligations + +- PULL remains typed read-only data in `graph/`; RENDER remains in `agents/contexts/` or adapter formatting. +- Do not widen into a generic records API beyond the list/related/overview shapes currently named by D60-L. + +### Expected touched paths (tentative) + +```pseudo +src/graph/ +├── snapshot.ts ~ +├── snapshot.test.ts ~ +└── README.md ~ +src/agents/contexts/ +├── graph.ts ~ +├── graph.test.ts ~ +├── node.ts ~ +└── node.test.ts ~ +src/.pi/extensions/graph/ +├── command-adapter.ts ~ +└── index.ts ~ +src/rpc/ +├── handlers.ts ? +└── handlers.test.ts ? +src/web/ +├── app.tsx ? +└── app.test.tsx ? +``` diff --git a/memory/cards/live-graph-observer--mise-en-place.md b/memory/cards/live-graph-observer--mise-en-place.md deleted file mode 100644 index 2e632813..00000000 --- a/memory/cards/live-graph-observer--mise-en-place.md +++ /dev/null @@ -1,115 +0,0 @@ -# Live graph observer mise en place - -Frontier: live-graph-observer | n/a -Status: active — Card 2 is documented; browser-observable smoke is blocked locally -Mode: chain -Created: 2026-06-03 - -## Orientation - -- Containing seam: product launch/setup around the `live-graph-observer` frontier; these cards prepare the branch identity and local manual loop without touching graph/RPC/web core paths. -- Frontier item: `live-graph-observer` (FE-795). This is branch-local mise en place, not a separate Linear issue or Graphite branch. -- Current card state: Card 1 is done; Card 2 has a documented CDP-first browser loop, but local browser automation is blocked until a Chrome/Playwright backend works. -- Main open risk: feedback-loop tooling can sprawl into a dev-platform project. Keep the workbench/tooling concrete enough to launch and observe the POC only. -- Cross-cutting obligations: preserve `.brunch/` as cwd-scoped durable state; do not commit generated `.brunch/data.db` or sessions; do not add compatibility aliases unless explicitly requested. - -## Card 1 — done — CLI identity and local workbench - -### Objective - -The project installs and launches as `brunch-cli` from a reusable in-repo POC workbench cwd. - -### Acceptance Criteria - -✓ `package.json` — package name is `brunch-cli`, version is at least `0.1.0`, and the only bin command is `brunch-cli`. -✓ `bin/brunch-cli.js` — the executable bin shim launches the built CLI, with no `brunch-next` bin alias left behind. -✓ `.fixtures/workbenches/live-graph-observer/` — contains a small committed README or marker explaining how to launch `brunch-cli` there and let `.brunch/` + `data.db` scaffold locally. -✓ `npm run build` or focused package/bin test — proves the renamed bin target is included and executable after build. - -### Verification Approach - -- Inner: focused package/bin test or build assertion — proves package identity and bin path. -- Middle: manual command from `.fixtures/workbenches/live-graph-observer/` — `brunch-cli --mode print` or `npm run dev -- --mode print` scaffolds `.brunch/` in that directory. - -### Cross-cutting obligations - -- `.brunch/` remains cwd-scoped and ignored; generated DB/session artifacts are not committed. -- Identity is singular: no `brunch-next` compatibility alias unless the user asks. - -### Assumption dependency - -None — this is setup identity work, not a product architecture claim. - -### Expected touched paths (tentative) - -```pseudo -package.json ~ -package-lock.json ~ -bin/ -├── brunch.js - -└── brunch-cli.js + -src/brunch.test.ts ? -.fixtures/workbenches/live-graph-observer/ -└── README.md + -``` - -### Promotion checklist - -- [ ] Does this change a requirement? -- [ ] Does this create, retire, or invalidate an assumption? -- [ ] Does this slice depend on an unvalidated high-impact assumption? -- [ ] Does this make or reverse a non-trivial design decision? -- [ ] Does this establish a new seam-level invariant? -- [ ] Does this change a frontier-level cross-cutting obligation or verification architecture layer? -- [ ] Does it cross more than two major seams? -- [ ] Is this the first touch in an unfamiliar seam from a fresh thread? -- [ ] Can you not name the containing seam or current rationale from the live docs? - -## Card 2 — blocked — Browser feedback loop decision - -### Objective - -The branch has one documented, runnable browser feedback loop for the web observer work. - -### Acceptance Criteria - -✓ Feedback-loop choice is explicit in the workbench README: recommended command(s), expected port/URL shape, and how to inspect browser console/network/accessibility state. -! Chrome/CDP command verification is blocked locally: the web host launches and prints a URL, but browser automation could not attach until a local Chrome/Playwright backend works. -✓ Browser automation/inspection tooling and `agentation` are treated as complementary: Chrome/CDP-style tooling observes the browser; `agentation` annotates the running browser so the agent can fetch annotations through its CLI. -n/a `agentation` is not enabled in this card, so no dependency/import change is recorded and no `src/web/*` edit is needed. -✓ No feedback-loop tool becomes product runtime behavior or a required POC dependency. - -### Verification Approach - -- Inner: doc/format verification for README/card changes; file-scoped lint/build for changed package/web files if a dev dependency or import is added. -- Middle: manual smoke in the workbench — launch host, open browser tooling, confirm the page is observable. -- Current observed state: `npm run build` passed and `brunch-cli --mode web` launched from the workbench; page-observable browser smoke is pending a working local browser backend. - -### Cross-cutting obligations - -- Keep feedback tooling out of canonical product state and out of `.brunch/` artifacts. -- Use Chrome/CDP-style tooling for browser inspection/automation and `agentation` for human/agent annotations when a running browser needs annotated UI feedback. - -### Assumption dependency - -None — if the tooling choice reveals a missing dev-server or MCP requirement, stop and rescope before adding a larger dev-platform seam. - -### Expected touched paths (tentative) - -```pseudo -.fixtures/workbenches/live-graph-observer/README.md ~ -package.json ? -package-lock.json ? -``` - -### Promotion checklist - -- [ ] Does this change a requirement? -- [ ] Does this create, retire, or invalidate an assumption? -- [ ] Does this slice depend on an unvalidated high-impact assumption? -- [ ] Does this make or reverse a non-trivial design decision? -- [ ] Does this establish a new seam-level invariant? -- [ ] Does this change a frontier-level cross-cutting obligation or verification architecture layer? -- [ ] Does it cross more than two major seams? -- [ ] Is this the first touch in an unfamiliar seam from a fresh thread? -- [ ] Can you not name the containing seam or current rationale from the live docs? diff --git a/package.json b/package.json index cdb477a9..0f02357e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "tsx src/brunch.ts", "build": "tsc -p tsconfig.build.json && npm run build:pi-assets && npm run build:web", - "build:pi-assets": "mkdir -p dist/.pi/components/workspace-dialog dist/.pi/context/prompt-packs && cp -R src/.pi/components/workspace-dialog/assets dist/.pi/components/workspace-dialog/ && cp src/.pi/context/prompt-packs/*.md dist/.pi/context/prompt-packs/", + "build:pi-assets": "mkdir -p dist/.pi/components/workspace-dialog dist/agents && cp -R src/.pi/components/workspace-dialog/assets dist/.pi/components/workspace-dialog/ && cp -R src/agents/definitions src/agents/goals src/agents/strategies src/agents/lenses src/agents/methods dist/agents/", "build:web": "vite build", "db:generate": "drizzle-kit generate", "db:studio": "drizzle-kit studio", diff --git a/src/.pi/README.md b/src/.pi/README.md index 6d873bdb..3779148b 100644 --- a/src/.pi/README.md +++ b/src/.pi/README.md @@ -3,10 +3,12 @@ This directory is intentionally shaped like a project-local Pi resource tree so Brunch-owned extensions can be hot-reloaded while developing TUI affordances. ```bash -cd src/tui-client +cd src pi # edit .pi/extensions/... or .pi/components/... /reload ``` Production Brunch does not rely on ambient discovery from the repository root. The product shell imports these modules explicitly; tests for extensions/components live in `.pi/__tests__/`, not inside auto-discovered resource directories. + +Prompting is adapter-only here: `extensions/prompting.ts` handles Pi `before_agent_start` and delegates composition to `src/agents/compose.ts` with explicit selected-spec/workspace context. Prompt resources and context renderers live under `src/agents/`; `.pi/` must not carry prompt-pack sources. diff --git a/src/.pi/__tests__/extension-registry.test.ts b/src/.pi/__tests__/extension-registry.test.ts index a6277157..a0267370 100644 --- a/src/.pi/__tests__/extension-registry.test.ts +++ b/src/.pi/__tests__/extension-registry.test.ts @@ -7,6 +7,13 @@ import { describe, expect, it } from 'vitest'; import alternatives from '../extensions/alternatives.js'; import chrome from '../extensions/chrome.js'; import commandPolicy from '../extensions/command-policy.js'; +import commands, { + BRUNCH_CONTINUE_COMMAND, + BRUNCH_LENS_COMMAND, + BRUNCH_MODE_COMMAND, + BRUNCH_STRATEGY_COMMAND, + BRUNCH_SWITCH_COMMAND, +} from '../extensions/commands.js'; import mentionAutocomplete from '../extensions/mention-autocomplete.js'; import operationalMode from '../extensions/operational-mode.js'; import prompting from '../extensions/prompting.js'; @@ -18,19 +25,18 @@ import structuredExchange, { REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, } from '../extensions/structured-exchange/index.js'; -import workspaceDialog, { BRUNCH_WORKSPACE_COMMAND } from '../extensions/workspace-dialog.js'; import { createBrunchPiExtensionShell } from '../pi-extension-shell.js'; const extensionDefaults = { 'alternatives.ts': alternatives, 'chrome.ts': chrome, 'command-policy.ts': commandPolicy, + 'commands.ts': commands, 'mention-autocomplete.ts': mentionAutocomplete, 'operational-mode.ts': operationalMode, 'prompting.ts': prompting, 'session-lifecycle.ts': sessionLifecycle, 'structured-exchange/index.ts': structuredExchange, - 'workspace-dialog.ts': workspaceDialog, }; describe('Brunch explicit Pi extension registry', () => { @@ -60,7 +66,13 @@ describe('Brunch explicit Pi extension registry', () => { REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, ]); - expect(recording.commandNames).toEqual([BRUNCH_WORKSPACE_COMMAND]); + expect(recording.commandNames).toEqual([ + BRUNCH_SWITCH_COMMAND, + BRUNCH_CONTINUE_COMMAND, + BRUNCH_LENS_COMMAND, + BRUNCH_STRATEGY_COMMAND, + BRUNCH_MODE_COMMAND, + ]); expect(recording.messageRenderers).toEqual(['alternatives-card-set']); expect(recording.shortcuts).toEqual(['ctrl+shift+b']); expect(recording.eventNames).toEqual([ diff --git a/src/.pi/__tests__/graph-tools.test.ts b/src/.pi/__tests__/graph-tools.test.ts index 1ca22651..cca395fe 100644 --- a/src/.pi/__tests__/graph-tools.test.ts +++ b/src/.pi/__tests__/graph-tools.test.ts @@ -275,6 +275,48 @@ describe('graph tools end-to-end', () => { } }); + it('read_graph neighborhood returns node-context markdown and typed details through the tool path', async () => { + const input = translateCommitGraph( + { + nodes: [ + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'Tool-visible goal', body: 'Selected body' }, + ], + edges: [], + }, + specId, + ); + const commitResult = executor.commitGraph(input); + expect(commitResult.status).toBe('success'); + if (commitResult.status !== 'success') return; + + const tools = new Map }>(); + registerBrunchGraph( + { + registerTool(tool: { name: string; execute(toolCallId: string, params: unknown): Promise }) { + tools.set(tool.name, tool); + }, + } as never, + { specId, commandExecutor: executor, snapshots }, + ); + + const result = (await tools.get('read_graph')!.execute('read-1', { + mode: 'neighborhood', + node_id: commitResult.nodes['n1'], + })) as { + content: Array<{ type: 'text'; text: string }>; + details: unknown; + }; + + expect(result.content[0]?.text).toContain('[Selected-spec node context]'); + expect(result.content[0]?.text).toContain('Tool-visible goal'); + expect(result.details).toMatchObject({ + status: 'success', + anchor: { title: 'Tool-visible goal' }, + neighbors: [], + edges: [], + }); + }); + it('read_graph neighborhood for missing node returns not_found', () => { const result = snapshots.getNodeNeighborhood(999); const text = formatNeighborhoodResult(result); diff --git a/src/.pi/__tests__/mention-autocomplete.test.ts b/src/.pi/__tests__/mention-autocomplete.test.ts index 70a1e8c0..f3046ab6 100644 --- a/src/.pi/__tests__/mention-autocomplete.test.ts +++ b/src/.pi/__tests__/mention-autocomplete.test.ts @@ -9,9 +9,8 @@ import { describe('Brunch mention autocomplete', () => { it('adds graph mention prompt guidance', async () => { - const beforeAgentStart: Array< - (event: { systemPrompt: string }, ctx: FakeExtensionContext) => Promise | unknown - > = []; + const beforeAgentStart: Array<(event: { systemPrompt: string }, ctx: FakeExtensionContext) => unknown> = + []; registerBrunchMentionAutocomplete({ on: (event: string, handler: never) => { diff --git a/src/.pi/__tests__/operational-mode.test.ts b/src/.pi/__tests__/operational-mode.test.ts index 48501e38..47336657 100644 --- a/src/.pi/__tests__/operational-mode.test.ts +++ b/src/.pi/__tests__/operational-mode.test.ts @@ -136,8 +136,10 @@ describe('Brunch agent runtime-state projection', () => { 'grep', 'find', 'ls', - 'structured_exchange', - 'present_alternatives', + 'present_question', + 'present_options', + 'read_graph', + 'commit_graph', 'bash', 'edit', 'write', @@ -162,7 +164,7 @@ describe('Brunch agent runtime-state projection', () => { ); expect(activeTools).toEqual([ - ['read', 'grep', 'find', 'ls', 'structured_exchange', 'present_alternatives'], + ['read', 'grep', 'find', 'ls', 'present_question', 'present_options', 'read_graph'], ]); expect(promptResult).toBeUndefined(); for (const toolName of ['bash', 'edit', 'write']) { @@ -172,7 +174,7 @@ describe('Brunch agent runtime-state projection', () => { }); } await expect( - Promise.resolve(events.tool_call?.({ toolName: 'structured_exchange' } as never)), + Promise.resolve(events.tool_call?.({ toolName: 'read_graph' } as never)), ).resolves.toBeUndefined(); expect(events.user_bash?.({ command: 'rm -rf .' } as never)).toMatchObject({ result: { diff --git a/src/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts index 1f0a616a..431fa63a 100644 --- a/src/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -4,11 +4,14 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { composeBrunchPrompt } from '../context/compose-brunch-prompt.js'; +import { composeAgentPrompt } from '../../agents/compose.js'; +import type { ReadinessGrade } from '../../agents/state.js'; +import type { WorkspacePostureState } from '../../session/workspace-session-coordinator.js'; import { BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, DEFAULT_BRUNCH_AGENT_STATE, appendBrunchAgentRuntimeSwitch, + projectBrunchAgentState, type BrunchAgentState, type BrunchAgentStateEntryData, registerBrunchOperationalModePolicy, @@ -46,44 +49,97 @@ class FakeRuntimeStateSessionManager { } } +const promptContext = { + spec: { id: 1, name: 'Spec', readinessGrade: 'commitments_ready' as const }, + workspace: { + cwd: '/tmp/brunch', + posture: workspacePosture({ + certainty: 'proving', + stakes: 'high', + audience: 'internal', + horizon: 'current-milestone', + migration: 'free-rewrite', + sourcing: 'strip-or-build', + }), + }, + session: { id: 'session-1', label: 'Session' }, + graphSnapshots: { + getGraphOverview: () => ({ + lsn: 4, + nodeCount: 2, + edgeCount: 1, + nodes: [ + { + id: 1, + specId: 1, + plane: 'intent' as const, + kind: 'goal' as const, + title: 'Clarify Brunch prompt posture', + basis: 'explicit' as const, + createdAtLsn: 2, + updatedAtLsn: 2, + }, + { + id: 2, + specId: 1, + plane: 'design' as const, + kind: 'module' as const, + title: 'Agent context renderer', + basis: 'explicit' as const, + createdAtLsn: 3, + updatedAtLsn: 3, + }, + ], + edges: [ + { + id: 1, + specId: 1, + category: 'realization' as const, + sourceId: 2, + targetId: 1, + basis: 'explicit' as const, + createdAtLsn: 4, + updatedAtLsn: 4, + }, + ], + }), + getNodeNeighborhood: () => ({ status: 'not_found' as const }), + }, +}; + +function workspacePosture(posture: WorkspacePostureState): WorkspacePostureState { + return posture; +} + describe('Brunch prompt-pack topology', () => { - it('composes deterministic private prompt packs in stable order', () => { - const result = composeBrunchPrompt({ - operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'step-wise-decision-tree', - agentLens: 'intent', - agentGoal: 'auto', + it('composes gated Brunch resource manifests instead of eager private prompt packs', () => { + const result = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([ + runtimeEntry({ + ...DEFAULT_BRUNCH_AGENT_STATE, + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'auto', + }), + ]), + spec: promptContext.spec, + workspace: promptContext.workspace, activeTools: ['read', 'grep', 'present_options'], }); - expect(result.packIds).toEqual([ - 'brunch-base', - 'elicit', - 'elicitor', - 'structured-exchange', - 'candidate-proposals', - 'capture-analysis', - ]); - expect(result.prompt).toContain('[Brunch agent state]'); - expect(result.prompt).toContain('Operational mode: elicit.'); - expect(result.prompt).toContain('Agent role: elicitor.'); - expect(result.prompt).toContain('Agent goal: auto.'); - expect(result.prompt).toContain('Agent strategy: step-wise-decision-tree.'); - expect(result.prompt).toContain('Agent lens: intent.'); - expect(result.prompt).toContain('Brunch exposes only elicit-safe tools: read, grep, present_options.'); - expect(result.prompt.indexOf('# Brunch base')).toBeLessThan( - result.prompt.indexOf('# Operational mode: elicit'), - ); - expect(result.prompt.indexOf('# Structured exchanges')).toBeLessThan( - result.prompt.indexOf('# Candidate proposals'), - ); - expect(result.prompt).toContain('Request outcomes are an exactly-one property-presence union'); - expect(result.prompt).toContain( - '`graph_refs` are per-candidate and strictly existing graph node references', - ); - expect(result.prompt).toContain('Capture is transcript-native analysis, not graph mutation.'); - expect(result.prompt).not.toContain('CommandExecutor result shapes'); + expect(result.prompt).toContain('[Brunch agent control]'); + expect(result.prompt).toContain('- op_mode: elicit'); + expect(result.prompt).toContain('- goal: auto'); + expect(result.prompt).toContain('- strategy: step-wise-decision-tree'); + expect(result.prompt).toContain('- lens: intent'); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain('name="step-wise-decision-tree"'); + expect(result.prompt).not.toContain('# Brunch base'); + expect(result.prompt).not.toContain('Request outcomes are an exactly-one property-presence union'); }); it('appends composed Brunch prompting from runtime-state projection', async () => { @@ -95,15 +151,18 @@ describe('Brunch prompt-pack topology', () => { }; const events: Record unknown> = {}; - registerBrunchPrompting({ - on: (event: string, handler: (event: never, ctx?: never) => unknown) => { - events[event] = handler; - }, - getAllTools: () => - ['read', 'grep', 'bash', 'write', 'present_options'].map((name) => ({ - name, - })), - } as never); + registerBrunchPrompting( + { + on: (event: string, handler: (event: never, ctx?: never) => unknown) => { + events[event] = handler; + }, + getAllTools: () => + ['read', 'grep', 'bash', 'write', 'present_options'].map((name) => ({ + name, + })), + } as never, + promptContext, + ); const result = await Promise.resolve( events.before_agent_start?.( @@ -117,18 +176,105 @@ describe('Brunch prompt-pack topology', () => { ); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining('base\n\n[Brunch agent state]'), + systemPrompt: expect.stringContaining('base\n\n[Brunch agent control]'), }); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining('Agent strategy: step-wise-disambiguate.'), + systemPrompt: expect.stringContaining('- strategy: step-wise-disambiguate'), }); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining( - 'Brunch exposes only elicit-safe tools: read, grep, present_options.', - ), + systemPrompt: expect.stringContaining('- active tools: read, grep, present_options'), + }); + expect(result).toMatchObject({ + systemPrompt: expect.stringContaining('[Selected-spec graph context · design lens]'), + }); + expect(result).toMatchObject({ + systemPrompt: expect.stringContaining('design modules/interfaces'), }); }); + it('refreshes selected-spec prompt context through the shell session-boundary path before composing', async () => { + const events: Record unknown>> = {}; + let selected = { + spec: { id: 1, name: 'Launch spec', readinessGrade: 'commitments_ready' as const }, + session: { id: 'launch-session', label: 'Launch session' }, + nodeTitles: ['Launch-only node'], + }; + + await createBrunchPiExtensionShell( + { + cwd: '/tmp/brunch', + chatMode: 'responding-to-elicitation', + phase: 'elicitation', + spec: { id: 1, title: 'Launch spec' }, + session: { id: 'launch-session', label: 'Launch session' }, + }, + async () => { + selected = { + spec: { id: 2, name: 'Switched spec', readinessGrade: 'commitments_ready' as const }, + session: { id: 'switched-session', label: 'Switched session' }, + nodeTitles: ['Switched current node'], + }; + }, + { + coordinator: {} as never, + graphMentionSource: { listMentionCandidates: () => [] }, + promptContext: () => ({ + spec: selected.spec, + workspace: promptContext.workspace, + session: selected.session, + graphSnapshots: { + getGraphOverview: () => ({ + lsn: 1, + nodeCount: selected.nodeTitles.length, + edgeCount: 0, + nodes: selected.nodeTitles.map((title, index) => ({ + id: index + 1, + specId: selected.spec.id, + plane: 'intent' as const, + kind: 'goal' as const, + title, + basis: 'explicit' as const, + createdAtLsn: 1, + updatedAtLsn: 1, + })), + edges: [], + }), + getNodeNeighborhood: () => ({ status: 'not_found' as const }), + }, + }), + }, + )({ + on: (eventName: string, handler: (event: never, ctx?: never) => unknown) => { + events[eventName] ??= []; + events[eventName].push(handler); + }, + registerTool() {}, + registerCommand() {}, + registerShortcut() {}, + registerMessageRenderer() {}, + sendMessage() {}, + getAllTools: () => ['read', 'grep'].map((name) => ({ name })), + setActiveTools() {}, + } as never); + + const results: unknown[] = []; + for (const handler of events.before_agent_start ?? []) { + results.push( + await Promise.resolve( + handler({ systemPrompt: 'base' } as never, { sessionManager: { getEntries: () => [] } } as never), + ), + ); + } + const promptResult = results.find( + (result) => typeof (result as { systemPrompt?: unknown } | undefined)?.systemPrompt === 'string', + ) as { systemPrompt: string } | undefined; + + expect(promptResult?.systemPrompt).toContain('- spec: Switched spec (#2)'); + expect(promptResult?.systemPrompt).toContain('Switched current node'); + expect(promptResult?.systemPrompt).not.toContain('Launch spec (#1)'); + expect(promptResult?.systemPrompt).not.toContain('Launch-only node'); + }); + it('derives prompt and active tools from the same transcript-backed runtime state', async () => { const manager = new FakeRuntimeStateSessionManager(); const events: Record unknown>> = {}; @@ -141,11 +287,13 @@ describe('Brunch prompt-pack topology', () => { }, registerTool: (_tool: { name: string }) => {}, getAllTools: () => - ['read', 'grep', 'bash', 'edit', 'write', 'present_options'].map((name) => ({ name })), + ['read', 'grep', 'bash', 'edit', 'write', 'present_options', 'read_graph', 'commit_graph'].map( + (name) => ({ name }), + ), setActiveTools: (tools: string[]) => activeTools.push(tools), }; registerBrunchOperationalModePolicy(pi as never); - registerBrunchPrompting(pi as never); + registerBrunchPrompting(pi as never, promptContext); for (const handler of events.session_start ?? []) { await handler({} as never, { sessionManager: manager } as never); @@ -186,20 +334,65 @@ 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', 'grep', 'present_options'], - ['read', 'grep', 'present_options'], + ['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'], ]); expect(defaultPrompt).toMatchObject({ - systemPrompt: expect.stringContaining('Agent strategy: auto.'), + systemPrompt: expect.stringContaining('- strategy: auto'), + }); + expect(switchedPrompt).toMatchObject({ + systemPrompt: expect.stringContaining('- strategy: propose-graph'), + }); + expect(defaultPrompt).toMatchObject({ + systemPrompt: expect.stringContaining( + '- active tools: read, grep, present_options, read_graph, commit_graph', + ), + }); + expect(defaultPrompt).toMatchObject({ + systemPrompt: expect.stringContaining('[Selected-spec graph context · auto lens]'), }); expect(switchedPrompt).toMatchObject({ - systemPrompt: expect.stringContaining('Agent strategy: propose-graph.'), + systemPrompt: expect.stringContaining('[Selected-spec graph context · oracle lens]'), }); }); - it('is registered by the explicit shell after operational-mode policy', async () => { + it('applies the selected-spec grade to commit_graph tool activation', async () => { + async function activeToolsForGrade(readinessGrade: ReadinessGrade) { + const events: Record unknown> = {}; + const activeTools: string[][] = []; + registerBrunchPrompting( + { + on: (event: string, handler: (event: never, ctx?: never) => unknown) => { + events[event] = handler; + }, + getAllTools: () => ['read', 'grep', 'read_graph', 'commit_graph'].map((name) => ({ name })), + setActiveTools: (tools: string[]) => activeTools.push(tools), + } as never, + { + ...promptContext, + spec: { ...promptContext.spec, readinessGrade }, + }, + ); + + await Promise.resolve( + events.before_agent_start?.( + { systemPrompt: 'base' } as never, + { sessionManager: { getEntries: () => [] } } as never, + ), + ); + return activeTools.at(-1) ?? []; + } + + await expect(activeToolsForGrade('grounding_onboarding')).resolves.not.toContain('commit_graph'); + await expect(activeToolsForGrade('elicitation_ready')).resolves.toContain('commit_graph'); + }); + + it('is registered by the explicit shell after operational-mode policy and appends composed manifests', async () => { const eventNames: string[] = []; + const events: Record unknown>> = {}; await createBrunchPiExtensionShell( { @@ -213,9 +406,14 @@ describe('Brunch prompt-pack topology', () => { { coordinator: {} as never, graphMentionSource: { listMentionCandidates: () => [] }, + promptContext, }, )({ - on: (eventName: string) => eventNames.push(eventName), + on: (eventName: string, handler: (event: never, ctx?: never) => unknown) => { + eventNames.push(eventName); + events[eventName] ??= []; + events[eventName].push(handler); + }, registerTool() {}, registerCommand() {}, registerShortcut() {}, @@ -229,23 +427,131 @@ describe('Brunch prompt-pack topology', () => { const userBashPolicyIndex = eventNames.indexOf('user_bash'); const promptingIndex = eventNames.indexOf('before_agent_start', userBashPolicyIndex + 1); const nextBeforeAgentStartIndex = eventNames.indexOf('before_agent_start', promptingIndex + 1); + const switchedState: BrunchAgentState = { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentStrategy: 'step-wise-disambiguate', + agentLens: 'design', + agentGoal: 'elicit-expand', + }; + const promptResults = await Promise.all( + (events.before_agent_start ?? []).map((handler) => + Promise.resolve( + handler( + { systemPrompt: 'base' } as never, + { sessionManager: { getEntries: () => [runtimeEntry(switchedState)] } } as never, + ), + ), + ), + ); + const promptResult = promptResults.find( + (result) => typeof (result as { systemPrompt?: unknown } | undefined)?.systemPrompt === 'string', + ); expect(operationalToolPolicyIndex).toBeGreaterThan(-1); expect(userBashPolicyIndex).toBeGreaterThan(operationalToolPolicyIndex); expect(promptingIndex).toBeGreaterThan(userBashPolicyIndex); expect(promptingIndex).toBeLessThan(nextBeforeAgentStartIndex); + expect(promptResult).toMatchObject({ + systemPrompt: expect.stringContaining(''), + }); + expect(promptResult).toMatchObject({ + systemPrompt: expect.stringContaining('name="step-wise-disambiguate"'), + }); + }); + + it('proves transcript-backed strategy and lens switches change product prompt posture', async () => { + const events: Record unknown>> = {}; + + await createBrunchPiExtensionShell( + { + cwd: '/tmp/brunch', + chatMode: 'responding-to-elicitation', + phase: 'elicitation', + spec: { id: 1, title: 'Spec' }, + session: { id: 'session-1', label: 'Session' }, + }, + undefined, + { + coordinator: {} as never, + graphMentionSource: { listMentionCandidates: () => [] }, + promptContext, + }, + )({ + on: (eventName: string, handler: (event: never, ctx?: never) => unknown) => { + events[eventName] ??= []; + events[eventName].push(handler); + }, + registerTool() {}, + registerCommand() {}, + registerShortcut() {}, + registerMessageRenderer() {}, + sendMessage() {}, + getAllTools: () => ['read', 'grep', 'present_options'].map((name) => ({ name })), + setActiveTools() {}, + } as never); + + async function promptFor(state: BrunchAgentState): Promise { + const results = await Promise.all( + (events.before_agent_start ?? []).map((handler) => + Promise.resolve( + handler( + { systemPrompt: 'base' } as never, + { sessionManager: { getEntries: () => [runtimeEntry(state)] } } as never, + ), + ), + ), + ); + const promptResult = results.find( + (result) => typeof (result as { systemPrompt?: unknown } | undefined)?.systemPrompt === 'string', + ) as { systemPrompt: string } | undefined; + return promptResult?.systemPrompt ?? ''; + } + + const disambiguateIntentPrompt = await promptFor({ + ...DEFAULT_BRUNCH_AGENT_STATE, + agentStrategy: 'step-wise-disambiguate', + agentLens: 'intent', + agentGoal: 'elicit-expand', + }); + const proposeDesignPrompt = await promptFor({ + ...DEFAULT_BRUNCH_AGENT_STATE, + agentStrategy: 'propose-graph', + agentLens: 'design', + agentGoal: 'elicit-expand', + }); + const acceptedBlindSpots = [ + 'prompt/body quality is fitness evidence', + 'graph-write reliability remains with graph-tool-resilience', + 'capture quality remains with capture-response-to-graph', + ]; + + expect(disambiguateIntentPrompt).toContain('name="step-wise-disambiguate"'); + expect(disambiguateIntentPrompt).not.toContain('name="propose-graph"'); + expect(proposeDesignPrompt).toContain('name="propose-graph"'); + expect(proposeDesignPrompt).not.toContain('name="step-wise-disambiguate"'); + expect(disambiguateIntentPrompt).toContain('[Selected-spec graph context · intent lens]'); + expect(disambiguateIntentPrompt).toContain('intent claims, terms, assumptions'); + expect(proposeDesignPrompt).toContain('[Selected-spec graph context · design lens]'); + expect(proposeDesignPrompt).toContain('design modules/interfaces'); + expect(disambiguateIntentPrompt).toContain('Clarify Brunch prompt posture'); + expect(proposeDesignPrompt).toContain('Clarify Brunch prompt posture'); + expect(acceptedBlindSpots).toEqual([ + 'prompt/body quality is fitness evidence', + 'graph-write reliability remains with graph-tool-resilience', + 'capture quality remains with capture-response-to-graph', + ]); }); - it('does not expose private prompt packs through Pi resource discovery', async () => { - const [promptingSource, composerSource] = await Promise.all([ + it('does not expose prompt manifests through Pi resource discovery or legacy context imports', async () => { + const [promptingSource, shellSource] = await Promise.all([ readFile(join(projectRoot(), 'src/.pi/extensions/prompting.ts'), 'utf8'), - readFile(join(projectRoot(), 'src/.pi/context/compose-brunch-prompt.ts'), 'utf8'), + readFile(join(projectRoot(), 'src/.pi/pi-extension-shell.ts'), 'utf8'), ]); expect(promptingSource).not.toContain('resources_discover'); expect(promptingSource).not.toContain('promptPaths'); - expect(composerSource).not.toContain('resources_discover'); - expect(composerSource).not.toContain('promptPaths'); + expect(promptingSource).not.toContain('compose-brunch-prompt'); + expect(shellSource).not.toContain('compose-brunch-prompt'); }); }); diff --git a/src/.pi/context/README.md b/src/.pi/context/README.md deleted file mode 100644 index a3633911..00000000 --- a/src/.pi/context/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Brunch private context and prompt packs - -This directory contains private Brunch product context and prompt assets for the embedded Pi runtime. - -It is intentionally under `.pi/context/`, not `.pi/prompts/`, so these files are not Pi prompt-template resources and are not user-invoked slash-command prompt templates. Product code imports and composes prompt packs deterministically through `compose-brunch-prompt.ts` and the `registerBrunchPrompting` extension. - -`prompt-packs/` contains Brunch-owned markdown fragments. They are product control-plane assets, not ambient Pi skills or templates, and must never be returned from `resources_discover.promptPaths`. - -Future dynamic context renderers live under `builders/`. Builders should project already-canonical state into prompt text; they must not become hidden stores, query ambient Pi resources, or invent uncaptured facts. diff --git a/src/.pi/context/builders/README.md b/src/.pi/context/builders/README.md deleted file mode 100644 index 39381bc1..00000000 --- a/src/.pi/context/builders/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Brunch context builders - -Builders are deterministic renderers over already-canonical state. They may later render graph, readiness, or structured-exchange snapshots into prompt context. - -Builders must not query ambient Pi resources, mutate graph truth, call the `CommandExecutor`, or invent uncaptured facts. If a fact is not already canonical or explicitly supplied in the builder snapshot, do not render it as product truth. diff --git a/src/.pi/context/builders/graph-context.ts b/src/.pi/context/builders/graph-context.ts deleted file mode 100644 index e49f7a9d..00000000 --- a/src/.pi/context/builders/graph-context.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface GraphContextSnapshot { - graphNodeCount?: number; -} - -export function renderGraphContext(_snapshot?: GraphContextSnapshot): string { - return ''; -} diff --git a/src/.pi/context/builders/readiness-context.ts b/src/.pi/context/builders/readiness-context.ts deleted file mode 100644 index 1741a313..00000000 --- a/src/.pi/context/builders/readiness-context.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ReadinessContextSnapshot { - readinessGrade?: string; - elicitationPosture?: string; -} - -export function renderReadinessContext(_snapshot?: ReadinessContextSnapshot): string { - return ''; -} diff --git a/src/.pi/context/builders/structured-exchange-context.ts b/src/.pi/context/builders/structured-exchange-context.ts deleted file mode 100644 index eb6acc48..00000000 --- a/src/.pi/context/builders/structured-exchange-context.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface StructuredExchangeContextSnapshot { - pendingExchangeId?: string; -} - -export function renderStructuredExchangeContext(_snapshot?: StructuredExchangeContextSnapshot): string { - return ''; -} diff --git a/src/.pi/context/compose-brunch-prompt.ts b/src/.pi/context/compose-brunch-prompt.ts deleted file mode 100644 index eb22dc46..00000000 --- a/src/.pi/context/compose-brunch-prompt.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { renderGraphContext } from './builders/graph-context.js'; -import { renderReadinessContext } from './builders/readiness-context.js'; -import { renderStructuredExchangeContext } from './builders/structured-exchange-context.js'; - -export interface BrunchPromptCompositionState { - operationalMode: string; - agentRole: string; - agentStrategy: string; - agentLens: string; - agentGoal: string; - activeTools: readonly string[]; -} - -export interface BrunchPromptPack { - id: string; - title: string; - markdown: string; -} - -export interface BrunchPromptCompositionResult { - prompt: string; - packIds: readonly string[]; -} - -const PROMPT_PACK_ORDER = [ - 'brunch-base', - 'elicit', - 'elicitor', - 'structured-exchange', - 'candidate-proposals', - 'capture-analysis', -] as const; - -type PromptPackId = (typeof PROMPT_PACK_ORDER)[number]; - -const PROMPT_PACK_TITLES: Record = { - 'brunch-base': 'Brunch base', - elicit: 'Operational mode: elicit', - elicitor: 'Agent role: elicitor', - 'structured-exchange': 'Structured exchanges', - 'candidate-proposals': 'Candidate proposals', - 'capture-analysis': 'Capture analysis', -}; - -function readPromptPack(id: PromptPackId): BrunchPromptPack { - return { - id, - title: PROMPT_PACK_TITLES[id], - markdown: readFileSync(new URL(`./prompt-packs/${id}.md`, import.meta.url), 'utf8').trim(), - }; -} - -const PROMPT_PACKS = PROMPT_PACK_ORDER.map(readPromptPack); - -function renderAgentState(state: BrunchPromptCompositionState): string { - const tools = state.activeTools.join(', ') || 'none'; - - return [ - '[Brunch agent state]', - `- Operational mode: ${state.operationalMode}.`, - `- Agent role: ${state.agentRole}.`, - `- Agent goal: ${state.agentGoal}.`, - `- Agent strategy: ${state.agentStrategy}.`, - `- Agent lens: ${state.agentLens}.`, - `- Prompt packs: ${PROMPT_PACK_ORDER.join(', ')}.`, - '', - '[Brunch tool policy]', - `- Brunch exposes only elicit-safe tools: ${tools}.`, - '- Do not attempt to write files, edit code, run shell commands, change git state, install dependencies, start processes, or mutate external systems.', - '- If the user asks for a side-effecting action, explain that this Brunch prototype is read-only for now.', - ].join('\n'); -} - -function joinPromptSections(sections: readonly string[]): string { - return sections - .map((section) => section.trim()) - .filter((section) => section.length > 0) - .join('\n\n'); -} - -export function composeBrunchPrompt(state: BrunchPromptCompositionState): BrunchPromptCompositionResult { - const packSections = PROMPT_PACKS.map((pack) => pack.markdown); - const dynamicSections = [renderGraphContext(), renderReadinessContext(), renderStructuredExchangeContext()]; - const prompt = joinPromptSections([renderAgentState(state), ...packSections, ...dynamicSections]); - - return { - prompt, - packIds: PROMPT_PACKS.map((pack) => pack.id), - }; -} diff --git a/src/.pi/context/prompt-packs/brunch-base.md b/src/.pi/context/prompt-packs/brunch-base.md deleted file mode 100644 index e4a05253..00000000 --- a/src/.pi/context/prompt-packs/brunch-base.md +++ /dev/null @@ -1,6 +0,0 @@ -# Brunch base - -- Brunch is an elicitation-first specification product built over Pi. -- Use Brunch product tools and Pi transcript truth; do not treat ambient free chat as the primary workflow. -- Do not expose Pi customization APIs, prompt templates, skills, themes, or extensions as Brunch product behavior. -- Do not mutate graph truth except through the future Brunch command layer and approved product tools. diff --git a/src/.pi/context/prompt-packs/capture-analysis.md b/src/.pi/context/prompt-packs/capture-analysis.md deleted file mode 100644 index b64496bd..00000000 --- a/src/.pi/context/prompt-packs/capture-analysis.md +++ /dev/null @@ -1,7 +0,0 @@ -# Capture analysis - -- `capture_*` follows `request_*`. -- For candidate selection, capture consumes the selected candidate `user_rubric`, selected candidate `meta_rubric`, selected candidate `graph_refs`, and the user's `comment` if present. -- Capture is transcript-native analysis, not graph mutation. -- Do not invent final graph payloads, LSNs, or `CommandExecutor` result shapes in this prompt pack. -- Future graph writes must route through `CommandExecutor`; this pack must not imply a bypass. diff --git a/src/.pi/context/prompt-packs/elicit.md b/src/.pi/context/prompt-packs/elicit.md deleted file mode 100644 index abd9b2a1..00000000 --- a/src/.pi/context/prompt-packs/elicit.md +++ /dev/null @@ -1,6 +0,0 @@ -# Operational mode: elicit - -- You are operating in `elicit` mode. -- Ask or present the next useful structured exchange. -- Keep side effects out of elicit mode. -- Prefer buildable, transcript-backed structured interactions over hidden state. diff --git a/src/.pi/context/prompt-packs/elicitor.md b/src/.pi/context/prompt-packs/elicitor.md deleted file mode 100644 index d77fb6b3..00000000 --- a/src/.pi/context/prompt-packs/elicitor.md +++ /dev/null @@ -1,6 +0,0 @@ -# Agent role: elicitor - -- The active role is `elicitor`. -- Own next-move selection; establishment offers orient rather than becoming a default menu. -- Use lenses as elicitor strategies, not agent roles. -- Preserve `lens` metadata where Brunch schemas or tools require it. diff --git a/src/.pi/context/prompt-packs/structured-exchange.md b/src/.pi/context/prompt-packs/structured-exchange.md deleted file mode 100644 index 334ed782..00000000 --- a/src/.pi/context/prompt-packs/structured-exchange.md +++ /dev/null @@ -1,11 +0,0 @@ -# Structured exchanges - -- Structured exchanges are transcript-native `present_* -> request_* -> capture_*` tool result families. -- `toolResult.content` is durable markdown for transcript display and model-readable context. -- `toolResult.details` is structured recovery and projection data. -- `renderCall` is not semantic and must not carry durable Brunch meaning. -- Classify structured-exchange rows by `details.schema`, not `toolName` alone. -- Use `schema` plus `v` as checked discriminants in the details model. -- Use `tool_meta` for sequence and sibling information. -- Use `comment` for user-authored text and `message` for system/runtime-authored text. -- Request outcomes are an exactly-one property-presence union: `answered`, `cancelled`, or `unavailable`. diff --git a/src/.pi/extensions/commands.ts b/src/.pi/extensions/commands.ts index 5a634603..88682d79 100644 --- a/src/.pi/extensions/commands.ts +++ b/src/.pi/extensions/commands.ts @@ -1,14 +1,99 @@ /** @file commands.ts * - * NOTES + * Registers Brunch's namespaced `/brunch:*` slash commands. + * + * Pi parses slash command names as everything between the leading `/` and the + * first whitespace (see `_tryExecuteExtensionCommand` in + * `@earendil-works/pi-coding-agent/dist/core/agent-session.js`). Colons in + * command names are passed through verbatim, so registering a command with the + * literal name `brunch:switch` makes it invocable as `/brunch:switch`. This is + * the same trick the built-in `/skill:` registry uses. + * + * Active commands: + * - `/brunch:switch` — open the spec/session picker (delegates to + * workspace-dialog.ts). + * + * Stubbed for later (notify-only): + * - `/brunch:continue` — recover/restart from an interrupted `request_*` tool + * or other interruption. Needs to: (a) optionally add a + * system-prompt hint that bare "continue" resumes the + * brunch flow, and (b) install listeners on user cancel + * actions that surface a `setStatus` reminder. + * - `/brunch:lens` — change agent lens. + * - `/brunch:strategy` — change agent strategy. + * - `/brunch:mode` — change agent mode. + */ -- can we get skill-like namespaced commands e.g. /brunch:continue, /brunch:switch - - investigate the source code to figure out how the skill/command matching is done - - hackable? --would look a little bit more branded -- /continue - - a slash command that you could use to continue, if interrupted/recovering - - a system prompt insertion that just says that "if you see the word continue on its own, it means to continue the brunch flow" - - should set listeners for *any user action that cancels the flow* (like the escape key or whatever) - - insert a `setStatus` line above the input reminding the user that they can type /continue to re-enter the brunch flow +import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent'; - */ +import { type BrunchSpecSessionPickerOptions, runBrunchWorkspaceAction } from './workspace-dialog.js'; + +export const BRUNCH_COMMAND_PREFIX = 'brunch:'; +export const BRUNCH_SWITCH_COMMAND = 'brunch:switch'; +export const BRUNCH_CONTINUE_COMMAND = 'brunch:continue'; +export const BRUNCH_LENS_COMMAND = 'brunch:lens'; +export const BRUNCH_STRATEGY_COMMAND = 'brunch:strategy'; +export const BRUNCH_MODE_COMMAND = 'brunch:mode'; + +export const BRUNCH_SWITCH_SHORTCUT = 'ctrl+shift+b'; + +export type BrunchCommandsOptions = BrunchSpecSessionPickerOptions; + +interface BrunchStubCommand { + readonly name: string; + readonly description: string; + readonly pendingMessage: string; +} + +const BRUNCH_STUB_COMMANDS: readonly BrunchStubCommand[] = [ + { + name: BRUNCH_CONTINUE_COMMAND, + description: 'Resume the Brunch flow after an interruption (not yet implemented)', + pendingMessage: '/brunch:continue is not wired up yet.', + }, + { + name: BRUNCH_LENS_COMMAND, + description: 'Change the active agent lens (not yet implemented)', + pendingMessage: '/brunch:lens is not wired up yet.', + }, + { + name: BRUNCH_STRATEGY_COMMAND, + description: 'Change the active agent strategy (not yet implemented)', + pendingMessage: '/brunch:strategy is not wired up yet.', + }, + { + name: BRUNCH_MODE_COMMAND, + description: 'Change the active agent mode (not yet implemented)', + pendingMessage: '/brunch:mode is not wired up yet.', + }, +]; + +export function registerBrunchCommands(pi: ExtensionAPI, { coordinator }: BrunchCommandsOptions): void { + pi.registerCommand(BRUNCH_SWITCH_COMMAND, { + description: 'Open the Brunch spec/session picker', + handler: async (_args, ctx: ExtensionCommandContext) => { + await runBrunchWorkspaceAction(ctx, coordinator); + }, + }); + + for (const command of BRUNCH_STUB_COMMANDS) { + pi.registerCommand(command.name, { + description: command.description, + handler: async (_args, ctx: ExtensionCommandContext) => { + ctx.ui.notify(command.pendingMessage, 'info'); + }, + }); + } + + pi.registerShortcut?.(BRUNCH_SWITCH_SHORTCUT, { + description: 'Open the Brunch spec/session picker', + handler: async (ctx) => { + ctx.ui.notify( + 'Use /brunch:switch to switch specs or sessions; Pi shortcut contexts cannot switch sessions yet.', + 'warning', + ); + }, + }); +} + +export default registerBrunchCommands; diff --git a/src/.pi/extensions/graph/index.ts b/src/.pi/extensions/graph/index.ts index 79d5bb32..ac9ebd47 100644 --- a/src/.pi/extensions/graph/index.ts +++ b/src/.pi/extensions/graph/index.ts @@ -12,15 +12,11 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; +import { renderNodeContext } from '../../../agents/contexts/node.js'; import type { CommandExecutor } from '../../../graph/command-executor.js'; import type { GraphOverview, NeighborhoodResult } from '../../../graph/snapshot.js'; import { graphMutationProductUpdates, type ProductUpdatePublisher } from '../../../rpc/product-updates.js'; -import { - translateCommitGraph, - formatCommitGraphResult, - formatGraphOverview, - formatNeighborhoodResult, -} from './command-adapter.js'; +import { translateCommitGraph, formatCommitGraphResult, formatGraphOverview } from './command-adapter.js'; import { CommitGraphParams, ReadGraphParams } from './tool-schemas.js'; // --------------------------------------------------------------------------- @@ -52,7 +48,7 @@ export interface BrunchGraphDeps { // --------------------------------------------------------------------------- export function registerBrunchGraph(pi: ExtensionAPI, deps: BrunchGraphDeps): void { - const { specId, commandExecutor, snapshots } = deps; + const { commandExecutor, snapshots } = deps; // ── commit_graph ──────────────────────────────────────────────────── pi.registerTool({ @@ -74,6 +70,7 @@ export function registerBrunchGraph(pi: ExtensionAPI, deps: BrunchGraphDeps): vo parameters: CommitGraphParams, async execute(_toolCallId, params) { + const specId = deps.specId; const input = translateCommitGraph(params, specId); const result = commandExecutor.commitGraph(input); const text = formatCommitGraphResult(result); @@ -105,24 +102,27 @@ export function registerBrunchGraph(pi: ExtensionAPI, deps: BrunchGraphDeps): vo async execute(_toolCallId, params) { let text: string; + let details: GraphOverview | NeighborhoodResult; if (params.mode === 'overview') { - text = formatGraphOverview(snapshots.getGraphOverview()); + const overview = snapshots.getGraphOverview(); + text = formatGraphOverview(overview); + details = overview; } else { if (params.node_id == null) { throw new Error('node_id is required for neighborhood mode'); } - text = formatNeighborhoodResult( - snapshots.getNodeNeighborhood( - params.node_id, - params.hops != null ? { hops: params.hops } : undefined, - ), + const neighborhood = snapshots.getNodeNeighborhood( + params.node_id, + params.hops != null ? { hops: params.hops } : undefined, ); + text = renderNodeContext(neighborhood); + details = neighborhood; } return { content: [{ type: 'text' as const, text }], - details: {}, + details, }; }, }); diff --git a/src/.pi/extensions/operational-mode.ts b/src/.pi/extensions/operational-mode.ts index 51285380..fff53cbb 100644 --- a/src/.pi/extensions/operational-mode.ts +++ b/src/.pi/extensions/operational-mode.ts @@ -19,6 +19,8 @@ import { } from '@earendil-works/pi-coding-agent'; import { Text } from '@earendil-works/pi-tui'; +import { activeToolNamesForPosture, type ReadinessGrade } from '../../agents/state.js'; + const ELICIT_BLOCKED_TOOLS = ['bash', 'edit', 'write'] as const; type ElicitBlockedToolName = (typeof ELICIT_BLOCKED_TOOLS)[number]; @@ -67,14 +69,6 @@ function shortenPath(path: string): string { return path; } -function elicitToolNames(pi: ExtensionAPI): string[] { - const blocked = new Set(ELICIT_BLOCKED_TOOLS); - return pi - .getAllTools() - .map((tool) => tool.name) - .filter((name) => !blocked.has(name)); -} - interface SessionManagerLike { getEntries(): readonly CustomEntryLike[]; } @@ -97,11 +91,13 @@ function supportsBrunchAgentStateEntries( export function activeToolNamesForBrunchAgentState( pi: ExtensionAPI, state: ResolvedBrunchAgentState, + readinessGrade: ReadinessGrade = 'grounding_onboarding', ): string[] { - if (state.operationalModeDefinition.toolPolicyId === 'elicit-read-only') { - return elicitToolNames(pi); - } - return []; + return activeToolNamesForPosture({ + registeredToolNames: pi.getAllTools().map((tool) => tool.name), + state, + readinessGrade, + }); } function isBlockedElicitTool(toolName: string): toolName is ElicitBlockedToolName { diff --git a/src/.pi/extensions/prompting.ts b/src/.pi/extensions/prompting.ts index 26444d6b..376eb225 100644 --- a/src/.pi/extensions/prompting.ts +++ b/src/.pi/extensions/prompting.ts @@ -1,6 +1,15 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; -import { composeBrunchPrompt } from '../context/compose-brunch-prompt.js'; +import { + composeAgentPrompt, + renderCwdContext, + renderGraphContext, + type AgentPromptSessionContext, + type AgentPromptSnapshotContext, + type AgentPromptSpecContext, + type AgentPromptWorkspaceContext, +} from '../../agents/index.js'; +import type { GraphSnapshotReaders } from './graph/index.js'; import { activeToolNamesForBrunchAgentState, projectBrunchAgentState } from './operational-mode.js'; type BrunchAgentStateEntries = Parameters[0]; @@ -17,6 +26,18 @@ interface BeforeAgentStartContextLike { sessionManager?: SessionManagerLike; } +export interface BrunchPromptContext { + spec: AgentPromptSpecContext; + workspace: AgentPromptWorkspaceContext; + session?: AgentPromptSessionContext; + snapshots?: AgentPromptSnapshotContext; + graphSnapshots?: GraphSnapshotReaders; +} + +export type BrunchPromptContextProvider = + | BrunchPromptContext + | (() => BrunchPromptContext | Promise); + function supportsPrompting(pi: ExtensionAPI): boolean { return typeof (pi as Partial).on === 'function'; } @@ -25,21 +46,33 @@ function projectState(ctx: BeforeAgentStartContextLike | undefined) { return projectBrunchAgentState(ctx?.sessionManager?.getEntries() ?? []); } -export function registerBrunchPrompting(pi: ExtensionAPI): void { +export function registerBrunchPrompting( + pi: ExtensionAPI, + promptContext: BrunchPromptContextProvider | undefined, +): 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' - ? activeToolNamesForBrunchAgentState(pi, state) + ? activeToolNamesForBrunchAgentState(pi, state, resolvedPromptContext.spec.readinessGrade) : []; - const { prompt } = composeBrunchPrompt({ - operationalMode: state.operationalMode, - agentRole: state.agentRole, - agentStrategy: state.agentStrategy, - agentLens: state.agentLens, - agentGoal: state.agentGoal, + if (typeof (pi as Partial).setActiveTools === 'function') { + pi.setActiveTools(activeTools); + } + const snapshots = snapshotsForPromptContext(resolvedPromptContext, state); + const { prompt } = composeAgentPrompt({ + agentId: state.agentRole, + sessionState: state, + spec: resolvedPromptContext.spec, + workspace: resolvedPromptContext.workspace, + snapshots, activeTools, }); @@ -52,4 +85,33 @@ export function registerBrunchPrompting(pi: ExtensionAPI): void { }); } +function snapshotsForPromptContext( + context: BrunchPromptContext, + state: ReturnType, +): AgentPromptSnapshotContext { + const renderedContexts = [ + renderCwdContext({ + spec: context.spec, + workspace: context.workspace, + ...(context.session ? { session: context.session } : {}), + }), + ]; + if (context.graphSnapshots) { + renderedContexts.push( + renderGraphContext(context.graphSnapshots.getGraphOverview(), { lens: state.agentLens }), + ); + } + + return { + ...(context.snapshots?.contextHandles ? { contextHandles: context.snapshots.contextHandles } : {}), + renderedContexts: [...(context.snapshots?.renderedContexts ?? []), ...renderedContexts], + }; +} + +async function resolvePromptContext( + promptContext: BrunchPromptContextProvider, +): Promise { + return typeof promptContext === 'function' ? promptContext() : promptContext; +} + export default registerBrunchPrompting; diff --git a/src/.pi/extensions/workspace-dialog.ts b/src/.pi/extensions/workspace-dialog.ts index 2cf01b00..1f12480e 100644 --- a/src/.pi/extensions/workspace-dialog.ts +++ b/src/.pi/extensions/workspace-dialog.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent'; +import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent'; import { type WorkspaceSessionReadyState, @@ -11,46 +11,10 @@ import { } from '../components/workspace-dialog/index.js'; import { chromeStateForWorkspace, renderBrunchChrome } from './chrome.js'; -export const BRUNCH_WORKSPACE_COMMAND = 'brunch'; -export const BRUNCH_WORKSPACE_SHORTCUT = 'ctrl+shift+b'; - export interface BrunchSpecSessionPickerOptions { coordinator: SpecSessionActivationCoordinator; } -export function registerBrunchWorkspaceDialog( - pi: ExtensionAPI, - { coordinator }: BrunchSpecSessionPickerOptions, -): void { - pi.registerCommand(BRUNCH_WORKSPACE_COMMAND, { - description: 'Open the Brunch spec/session picker', - handler: async (_args, ctx) => { - await runBrunchWorkspaceCommand(ctx, coordinator); - }, - }); - pi.registerShortcut?.(BRUNCH_WORKSPACE_SHORTCUT, { - description: 'Open the Brunch spec/session picker', - handler: async (ctx) => { - ctx.ui.notify( - 'Use /brunch to switch specs or sessions; Pi shortcut contexts cannot switch sessions yet.', - 'warning', - ); - }, - }); -} - -export default function brunchWorkspaceDialog(pi: ExtensionAPI): void { - pi.registerCommand(BRUNCH_WORKSPACE_COMMAND, { - description: 'Open the Brunch spec/session picker', - handler: async (_args, ctx) => { - ctx.ui.notify( - 'The Brunch workspace picker needs a product coordinator and is only available through the Brunch CLI.', - 'warning', - ); - }, - }); -} - export async function runBrunchWorkspaceCommand( ctx: ExtensionCommandContext, coordinator: SpecSessionActivationCoordinator, @@ -111,7 +75,7 @@ async function switchToActivatedWorkspace( ): Promise { if (typeof ctx.switchSession !== 'function') { ctx.ui.notify( - 'Use /brunch to switch specs or sessions; this Pi context cannot switch sessions.', + 'Use /brunch:switch to switch specs or sessions; this Pi context cannot switch sessions.', 'warning', ); return; diff --git a/src/.pi/pi-extension-shell.ts b/src/.pi/pi-extension-shell.ts index 4de54c4d..63bd27d9 100644 --- a/src/.pi/pi-extension-shell.ts +++ b/src/.pi/pi-extension-shell.ts @@ -4,6 +4,7 @@ import { registerBrunchAlternatives } from './extensions/alternatives.js'; import { registerBrunchChrome } from './extensions/chrome.js'; import { type BrunchChromeState } from './extensions/chrome.js'; import { registerBrunchBranchPolicyHandlers } from './extensions/command-policy.js'; +import { registerBrunchCommands, type BrunchCommandsOptions } from './extensions/commands.js'; import { registerBrunchGraph, type BrunchGraphDeps } from './extensions/graph/index.js'; import { type GraphMentionSource } from './extensions/mention-autocomplete.js'; import { @@ -11,12 +12,10 @@ import { registerBrunchMentionAutocomplete, } from './extensions/mention-autocomplete.js'; import { registerBrunchOperationalModePolicy } from './extensions/operational-mode.js'; -import { registerBrunchPrompting } from './extensions/prompting.js'; +import { registerBrunchPrompting, type BrunchPromptContextProvider } from './extensions/prompting.js'; import { registerBrunchSessionBoundary } from './extensions/session-lifecycle.js'; import { type BrunchSessionBoundaryHandler } from './extensions/session-lifecycle.js'; import { registerStructuredExchange } from './extensions/structured-exchange/index.js'; -import { type BrunchSpecSessionPickerOptions } from './extensions/workspace-dialog.js'; -import { registerBrunchWorkspaceDialog } from './extensions/workspace-dialog.js'; export { registerBrunchAlternatives } from './extensions/alternatives.js'; export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE } from './extensions/command-policy.js'; @@ -69,9 +68,17 @@ export { type BrunchSessionBoundaryHandler, } from './extensions/session-lifecycle.js'; export { - BRUNCH_WORKSPACE_COMMAND, - BRUNCH_WORKSPACE_SHORTCUT, - registerBrunchWorkspaceDialog, + BRUNCH_COMMAND_PREFIX, + BRUNCH_CONTINUE_COMMAND, + BRUNCH_LENS_COMMAND, + BRUNCH_MODE_COMMAND, + BRUNCH_STRATEGY_COMMAND, + BRUNCH_SWITCH_COMMAND, + BRUNCH_SWITCH_SHORTCUT, + registerBrunchCommands, + type BrunchCommandsOptions, +} from './extensions/commands.js'; +export { runBrunchWorkspaceAction, runBrunchWorkspaceCommand, type BrunchSpecSessionPickerOptions, @@ -83,9 +90,10 @@ export { type GraphSnapshotReaders, } from './extensions/graph/index.js'; -export interface BrunchPiExtensionShellOptions extends BrunchSpecSessionPickerOptions { +export interface BrunchPiExtensionShellOptions extends BrunchCommandsOptions { graphMentionSource?: GraphMentionSource; graph?: BrunchGraphDeps; + promptContext?: BrunchPromptContextProvider; } type BrunchProductExtensionRegistrar = (pi: ExtensionAPI) => void | Promise; @@ -102,11 +110,11 @@ export function createBrunchPiExtensionShell( (api) => registerBrunchChrome(api, chrome), registerBrunchBranchPolicyHandlers, registerBrunchOperationalModePolicy, - registerBrunchPrompting, + (api) => registerBrunchPrompting(api, options.promptContext), (api) => registerBrunchMentionAutocomplete(api, graphMentionSource), registerBrunchAlternatives, registerStructuredExchange, - (api) => registerBrunchWorkspaceDialog(api, options), + (api) => registerBrunchCommands(api, options), ...(options.graph ? [(api: ExtensionAPI) => registerBrunchGraph(api, options.graph!)] : []), ]; diff --git a/src/README.md b/src/README.md index 8210c1dd..d2f6a0a9 100644 --- a/src/README.md +++ b/src/README.md @@ -9,10 +9,12 @@ src/ │ └── extensions/ Pi registrars: agent tools, TUI commands, enhancements │ ├── agents/ Agent intelligence layer -│ ├── modes/ operational mode prompts and rules -│ ├── strategies/ interaction-shape prompts (propose-graph, project-graph, etc.) -│ ├── lenses/ topical-focus prompts (intent, design, oracle, etc.) -│ └── contexts/ snapshot orchestration (calls graph/ and session/) +│ ├── definitions/ keyed agent prompt definitions +│ ├── goals/ runtime goal resources +│ ├── strategies/ interaction-shape resources (propose-graph, project-graph, etc.) +│ ├── lenses/ topical-focus resources (intent, design, oracle, etc.) +│ ├── methods/ tool-routing and sequencing resources +│ └── contexts/ snapshot rendering over graph/session typed pulls │ ├── db/ Persistence substrate │ Drizzle schema, migrations, connection lifecycle @@ -61,6 +63,6 @@ the React client in `src/web/` (formerly `web-client/`); shared test helpers in `src/probes/`. The active workspace file is `.brunch/workspace.json` (`state.json` is retired). -Still pending: prompt composition under `src/.pi/context/` migrates to -`src/agents/` per D52-L — deferred until the agent-runtime vocabulary work, -since that move reconciles the strategy/lens model rather than just relocating it. +Prompt composition and prompt resources live in `src/agents/` per D52-L/D58-L. +The old `src/.pi/context/` prompt-pack subtree is retired; `.pi/` remains an +adapter layer only. diff --git a/src/agents/README.md b/src/agents/README.md index f54e4749..46ce02b9 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -102,19 +102,21 @@ agents/ - `.pi/extensions/` prompt registrar — calls `compose()` at turn boundaries. - `.pi/extensions/operational-mode.ts` — reads the state enums from `state.ts`. -## Migration from .pi/context/ (owned by frontier work, not yet done) - -`state.ts` enums land with **agent-runtime-vocabulary**; everything else (compose, -resources, contexts, the migration itself) lands with **agents-composition-layer**. - -| Current (.pi/context/) | Target | Kind | -|-------------------------------------------------|-------------------------------------|----------| -| `compose-brunch-prompt.ts` | `agents/compose.ts` | rewrite | -| `prompt-packs/{brunch-base,elicit,elicitor}.md` | `agents/definitions/elicitor.md` | fold | -| `prompt-packs/structured-exchange.md` | `agents/methods/run-structured-exchange.md` | fold | -| `prompt-packs/capture-analysis.md` | `agents/methods/infer-and-capture.md` | rehome | -| `prompt-packs/candidate-proposals.md` | `agents/methods/generate-proposal.md` | rehome | -| `builders/graph-context.ts` | `agents/contexts/graph.ts` | rewrite | -| `builders/readiness-context.ts` | (folded into compose runtime header)| retire | -| `builders/structured-exchange-context.ts` | `methods/run-structured-exchange.md`| retire/fold | -| — | `state.ts`, `index.ts`, `goals/*`, `methods/{read-snapshot,commit-graph,review-for-gaps}.md`, `definitions/reviewer.md`, `contexts/{cwd,node}.ts` | new | +## Migration from .pi/context/ (complete) + +Product prompting imports `agents/compose.ts`; prompt-resource metadata is +code-owned in `state.ts`; detailed prompt resources live under +`definitions/`, `goals/`, `strategies/`, `lenses/`, and `methods/`; context +rendering lives under `contexts/`. The old `src/.pi/context/` prompt-pack +subtree is deleted rather than retained as a compatibility path. + +| Former (.pi/context/) | Current home | +|-------------------------------------------------|-------------------------------------| +| `compose-brunch-prompt.ts` | `agents/compose.ts` | +| `prompt-packs/{brunch-base,elicit,elicitor}.md` | `agents/definitions/elicitor.md` | +| `prompt-packs/structured-exchange.md` | `agents/methods/run-structured-exchange.md` | +| `prompt-packs/capture-analysis.md` | `agents/methods/infer-and-capture.md` | +| `prompt-packs/candidate-proposals.md` | `agents/methods/generate-proposal.md` | +| `builders/graph-context.ts` | `agents/contexts/graph.ts` | +| `builders/readiness-context.ts` | `agents/compose.ts` runtime header | +| `builders/structured-exchange-context.ts` | `agents/methods/run-structured-exchange.md` | diff --git a/src/agents/architecture.test.ts b/src/agents/architecture.test.ts new file mode 100644 index 00000000..d8e36cdb --- /dev/null +++ b/src/agents/architecture.test.ts @@ -0,0 +1,74 @@ +import { readFile, readdir } from 'node:fs/promises'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const projectRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); +const legacyContextPath = join(projectRoot, 'src/.pi/context'); + +const legacyImportNeedles = [ + ['src', '.pi', 'context'].join('/'), + 'compose' + '-brunch-prompt', + ['context', 'prompt-packs'].join('/'), + ['context', 'builders'].join('/'), +]; + +const resourceExpectations = [ + { + file: 'src/agents/methods/run-structured-exchange.md', + needles: ['details.schema', 'schema` plus `v', 'answered`, `cancelled`, or `unavailable`'], + }, + { + file: 'src/agents/methods/infer-and-capture.md', + needles: ['transcript-native analysis', 'not graph mutation', 'must never imply a graph bypass'], + }, + { + file: 'src/agents/methods/generate-proposal.md', + needles: ['legibility_cost_of_knowing', 'core_bet', 'graph_refs', '`{ node_id: string }` only'], + }, +]; + +describe('agents topology', () => { + it('keeps prompt guidance in src/agents resources and removes the legacy .pi context source', async () => { + await expect(readdir(legacyContextPath)).rejects.toThrow(); + + for (const expectation of resourceExpectations) { + const content = await readFile(join(projectRoot, expectation.file), 'utf8'); + for (const needle of expectation.needles) { + expect(content).toContain(needle); + } + } + }); + + it('keeps product source imports free of legacy .pi context prompt paths', async () => { + const files = await listSourceFiles(join(projectRoot, 'src')); + + for (const file of files) { + const rel = relative(projectRoot, file); + if (rel.endsWith('.test.ts') || rel.includes('/__tests__/')) continue; + const content = await readFile(file, 'utf8'); + for (const needle of legacyImportNeedles) { + expect(content, `${rel} must not reference ${needle}`).not.toContain(needle); + } + } + }); +}); + +async function listSourceFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await listSourceFiles(path))); + continue; + } + if (entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name)) { + files.push(path); + } + } + + return files; +} diff --git a/src/agents/compose.test.ts b/src/agents/compose.test.ts new file mode 100644 index 00000000..663f28e5 --- /dev/null +++ b/src/agents/compose.test.ts @@ -0,0 +1,241 @@ +import { access } from 'node:fs/promises'; +import { dirname, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { DEFAULT_BRUNCH_AGENT_STATE, projectBrunchAgentState } from '../session/runtime-state.js'; +import type { WorkspacePostureState } from '../session/workspace-session-coordinator.js'; +import { composeAgentPrompt } from './compose.js'; + +const projectRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); + +const groundingSpec = { + id: 1, + name: 'Grounding Spec', + readinessGrade: 'grounding_onboarding' as const, +}; + +const elicitationSpec = { + id: 1, + name: 'Elicitation Spec', + readinessGrade: 'elicitation_ready' as const, +}; + +const workspace = { + cwd: '/work/brunch', + posture: workspacePosture({ + certainty: 'proving', + stakes: 'high', + audience: 'internal', + horizon: 'current-milestone', + migration: 'free-rewrite', + sourcing: 'strip-or-build', + }), +}; + +function workspacePosture(posture: WorkspacePostureState): WorkspacePostureState { + return posture; +} + +const snapshots = { + contextHandles: ['graph-overview: compact selected-spec graph summary available via snapshot tools'], + renderedContexts: ['[Selected-spec graph context · intent lens]\n- lsn: 7; nodes: 1; edges: 0'], +}; + +describe('composeAgentPrompt', () => { + it('emits control, runtime, context handles, and manifest families for default AUTO axes', () => { + const result = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([]), + spec: groundingSpec, + workspace, + snapshots, + activeTools: ['read', 'grep', 'present_options'], + }); + + expect(result.prompt).toContain('[Brunch agent control]'); + expect(result.prompt).toContain('- agent: elicitor'); + expect(result.prompt).toContain('[Brunch runtime state]'); + expect(result.prompt).toContain('- spec: Grounding Spec (#1), readiness_grade=grounding_onboarding'); + expect(result.prompt).toContain( + '- workspace posture: certainty=proving; stakes=high; audience=internal; horizon=current-milestone; migration=free-rewrite; sourcing=strip-or-build', + ); + expect(result.prompt).toContain('[Brunch pushed context]'); + expect(result.prompt).toContain('handle: graph-overview: compact selected-spec graph summary'); + expect(result.prompt).toContain('[Selected-spec graph context · intent lens]'); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain(''); + expect(result.prompt).toContain('name="grounding-advance"'); + expect(result.prompt).not.toContain('name="capture-posture"'); + expect(result.prompt).not.toContain('name="commit-converge"'); + }); + + it('surfaces rendered snapshot text and preserves manifest legality when lens changes', () => { + const intent = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([ + { + type: 'custom', + customType: 'brunch.agent_runtime_state', + data: { + schemaVersion: 1, + reason: 'switch', + source: 'user', + state: { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentLens: 'intent', + }, + }, + }, + ]), + spec: elicitationSpec, + workspace, + snapshots: { + renderedContexts: ['[Selected-spec graph context · intent lens]\n- emphasis: intent claims'], + }, + activeTools: ['read'], + }); + const design = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([ + { + type: 'custom', + customType: 'brunch.agent_runtime_state', + data: { + schemaVersion: 1, + reason: 'switch', + source: 'user', + state: { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentLens: 'design', + }, + }, + }, + ]), + spec: elicitationSpec, + workspace, + snapshots: { + renderedContexts: ['[Selected-spec graph context · design lens]\n- emphasis: design modules'], + }, + activeTools: ['read'], + }); + + expect(intent.prompt).toContain('[Selected-spec graph context · intent lens]'); + expect(design.prompt).toContain('[Selected-spec graph context · design lens]'); + expect(intent.manifests.methods.map((entry) => entry.name)).toEqual( + design.manifests.methods.map((entry) => entry.name), + ); + expect(intent.manifests.lenses.map((entry) => entry.name)).toEqual(['intent']); + expect(design.manifests.lenses.map((entry) => entry.name)).toEqual(['design']); + }); + + it('filters AUTO axes by grade and allow-list, while pinned legal axes point at only the pinned resource', () => { + const auto = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([ + { + type: 'custom', + customType: 'brunch.agent_runtime_state', + data: { + schemaVersion: 1, + reason: 'switch', + source: 'user', + state: { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentGoal: 'auto', + }, + }, + }, + ]), + spec: elicitationSpec, + workspace, + activeTools: ['read'], + }); + + expect(auto.manifests.goals.map((entry) => entry.name)).toEqual([ + 'grounding-advance', + 'elicit-expand', + 'capture-posture', + ]); + expect(auto.manifests.strategies.map((entry) => entry.name)).toEqual([ + 'step-wise-decision-tree', + 'step-wise-disambiguate', + 'propose-graph', + ]); + expect(auto.manifests.lenses.map((entry) => entry.name)).toEqual(['intent', 'design', 'oracle']); + + const pinned = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([ + { + type: 'custom', + customType: 'brunch.agent_runtime_state', + data: { + schemaVersion: 1, + reason: 'switch', + source: 'user', + state: { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentStrategy: 'step-wise-disambiguate', + agentLens: 'design', + agentGoal: 'elicit-expand', + }, + }, + }, + ]), + spec: elicitationSpec, + workspace, + activeTools: ['read'], + }); + + expect(pinned.manifests.goals.map((entry) => entry.name)).toEqual(['elicit-expand']); + expect(pinned.manifests.strategies.map((entry) => entry.name)).toEqual(['step-wise-disambiguate']); + expect(pinned.manifests.lenses.map((entry) => entry.name)).toEqual(['design']); + }); + + it('rejects illegal pinned grade-gated selections loudly', () => { + expect(() => + composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([ + { + type: 'custom', + customType: 'brunch.agent_runtime_state', + data: { + schemaVersion: 1, + reason: 'switch', + source: 'user', + state: { + ...DEFAULT_BRUNCH_AGENT_STATE, + agentGoal: 'commit-converge', + }, + }, + }, + ]), + spec: groundingSpec, + workspace, + activeTools: ['read'], + }), + ).toThrow( + 'Pinned goal "commit-converge" is not legal for elicitor in elicit at readiness grade grounding_onboarding.', + ); + }); + + it('advertises only readable src/agents resources without filesystem discovery', async () => { + const result = composeAgentPrompt({ + agentId: 'elicitor', + sessionState: projectBrunchAgentState([]), + spec: elicitationSpec, + workspace, + activeTools: ['read'], + }); + + for (const entry of Object.values(result.manifests).flat()) { + expect(relative(projectRoot, entry.location).startsWith('src/agents/')).toBe(true); + await expect(access(entry.location)).resolves.toBeUndefined(); + } + }); +}); diff --git a/src/agents/compose.ts b/src/agents/compose.ts new file mode 100644 index 00000000..2bfe5d0c --- /dev/null +++ b/src/agents/compose.ts @@ -0,0 +1,152 @@ +import type { ResolvedBrunchAgentState } from '../session/runtime-state.js'; +import type { WorkspacePostureState } from '../session/workspace-session-coordinator.js'; +import { + AGENT_PROMPT_DEFINITIONS, + manifestsForState, + type PromptManifests, + type ReadinessGrade, +} from './state.js'; + +export interface AgentPromptSpecContext { + id: number; + name: string; + readinessGrade: ReadinessGrade; +} + +export interface AgentPromptWorkspaceContext { + cwd: string; + posture?: Partial; +} + +export interface AgentPromptSnapshotContext { + contextHandles?: readonly string[]; + renderedContexts?: readonly string[]; +} + +export interface ComposeAgentPromptInput { + agentId: ResolvedBrunchAgentState['agentRole']; + sessionState: ResolvedBrunchAgentState; + spec: AgentPromptSpecContext; + workspace: AgentPromptWorkspaceContext; + snapshots?: AgentPromptSnapshotContext; + activeTools?: readonly string[]; +} + +export interface ComposeAgentPromptResult { + prompt: string; + manifests: PromptManifests; +} + +export function composeAgentPrompt(input: ComposeAgentPromptInput): ComposeAgentPromptResult { + if (input.agentId !== input.sessionState.agentRole) { + throw new Error( + `Prompt agent "${String(input.agentId)}" does not match runtime-derived role "${String(input.sessionState.agentRole)}".`, + ); + } + + const definition = AGENT_PROMPT_DEFINITIONS[input.agentId]; + const manifests = manifestsForState(input.sessionState, input.spec.readinessGrade); + const prompt = joinSections([ + renderAgentControl(input, definition), + renderRuntimeState(input), + renderPushedContext(input.snapshots), + renderManifestFamily('available_goals', manifests.goals), + renderManifestFamily('available_strategies', manifests.strategies), + renderManifestFamily('available_lenses', manifests.lenses), + renderManifestFamily('available_methods', manifests.methods), + renderRouterRules(input.sessionState), + ]); + + return { prompt, manifests }; +} + +function renderAgentControl( + input: ComposeAgentPromptInput, + definition: (typeof AGENT_PROMPT_DEFINITIONS)[ComposeAgentPromptInput['agentId']], +): string { + const tools = input.activeTools?.join(', ') || 'none'; + return [ + '[Brunch agent control]', + `- agent: ${definition.id}`, + `- foreground role: ${input.sessionState.agentRole} (derived from op_mode=${input.sessionState.operationalMode})`, + `- model: ${definition.model}; thinking: ${definition.thinking}`, + `- tool authority: ${definition.toolAuthority}`, + `- active tools: ${tools}`, + ].join('\n'); +} + +function renderRuntimeState(input: ComposeAgentPromptInput): string { + return [ + '[Brunch runtime state]', + `- op_mode: ${input.sessionState.operationalMode}`, + `- goal: ${input.sessionState.agentGoal}`, + `- strategy: ${input.sessionState.agentStrategy}`, + `- lens: ${input.sessionState.agentLens}`, + `- spec: ${input.spec.name} (#${input.spec.id}), readiness_grade=${input.spec.readinessGrade}`, + `- workspace: ${input.workspace.cwd}`, + `- workspace posture: ${renderPosture(input.workspace.posture)}`, + ].join('\n'); +} + +function renderPosture(posture: AgentPromptWorkspaceContext['posture']): string { + if (!posture) return 'unrecorded'; + const entries = Object.entries(posture).filter((entry): entry is [string, string] => + Boolean(entry[1]?.trim()), + ); + return entries.length > 0 ? entries.map(([key, value]) => `${key}=${value}`).join('; ') : 'unrecorded'; +} + +function renderPushedContext(snapshots: AgentPromptSnapshotContext | undefined): string { + const handles = snapshots?.contextHandles ?? []; + const renderedContexts = snapshots?.renderedContexts ?? []; + return [ + '[Brunch pushed context]', + ...(handles.length ? handles.map((handle) => `- handle: ${handle}`) : ['- handles: none pushed']), + ...(renderedContexts.length + ? ['- rendered snapshots:', ...renderedContexts.map(indentBlock)] + : ['- rendered snapshots: none pushed']), + ].join('\n'); +} + +function indentBlock(value: string): string { + return value + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); +} + +function renderManifestFamily(tag: string, entries: PromptManifests[keyof PromptManifests]): string { + return [ + `<${tag}>`, + ...entries.map( + (entry) => + ` `, + ), + ``, + ].join('\n'); +} + +function renderRouterRules(state: ResolvedBrunchAgentState): string { + return [ + '[Brunch prompt-resource routing]', + '- Use only resources advertised in the manifests above; do not infer availability from the filesystem.', + '- For AUTO axes, choose from the current manifest and read the selected resource before applying detailed behavior.', + '- For pinned axes, the singleton manifest entry is the selected resource.', + `- Current pins: goal=${state.agentGoal}; strategy=${state.agentStrategy}; lens=${state.agentLens}.`, + ].join('\n'); +} + +function joinSections(sections: readonly string[]): string { + return sections + .map((section) => section.trim()) + .filter(Boolean) + .join('\n\n'); +} + +function escapeXml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); +} diff --git a/src/agents/contexts/cwd.test.ts b/src/agents/contexts/cwd.test.ts new file mode 100644 index 00000000..46f16428 --- /dev/null +++ b/src/agents/contexts/cwd.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { renderCwdContext } from './cwd.js'; + +describe('renderCwdContext', () => { + it('renders selected-spec/session/posture facts without ambient resource discovery', () => { + const rendered = renderCwdContext({ + spec: { id: 42, name: 'Payments Spec', readinessGrade: 'elicitation_ready' }, + workspace: { + cwd: '/repo/product', + posture: { + certainty: 'proving', + stakes: 'high', + migration: 'free-rewrite', + }, + }, + session: { id: 'session-7', label: 'Grounding' }, + }); + + expect(rendered).toContain('- cwd: /repo/product'); + expect(rendered).toContain('- selected spec: Payments Spec (#42); readiness_grade=elicitation_ready'); + expect(rendered).toContain('- selected session: Grounding (session-7)'); + expect(rendered).toContain('certainty=proving; stakes=high; migration=free-rewrite'); + expect(rendered).toContain('ambient Pi resources: not scanned'); + expect(rendered).toContain('graph scope: selected spec only'); + expect(rendered).not.toContain('.pi/context'); + }); +}); diff --git a/src/agents/contexts/cwd.ts b/src/agents/contexts/cwd.ts new file mode 100644 index 00000000..73f6cd88 --- /dev/null +++ b/src/agents/contexts/cwd.ts @@ -0,0 +1,38 @@ +import type { AgentPromptSpecContext, AgentPromptWorkspaceContext } from '../compose.js'; + +export interface AgentPromptSessionContext { + readonly id?: string; + readonly label?: string; +} + +export interface RenderCwdContextInput { + readonly spec: AgentPromptSpecContext; + readonly workspace: AgentPromptWorkspaceContext; + readonly session?: AgentPromptSessionContext; +} + +export function renderCwdContext(input: RenderCwdContextInput): string { + return [ + '[Selected workspace context]', + `- cwd: ${input.workspace.cwd}`, + `- selected spec: ${input.spec.name} (#${input.spec.id}); readiness_grade=${input.spec.readinessGrade}`, + `- selected session: ${renderSession(input.session)}`, + `- workspace posture: ${renderPosture(input.workspace.posture)}`, + '- ambient Pi resources: not scanned; Brunch prompt resources come only from code-owned manifests', + '- graph scope: selected spec only; no workspace-global graph fallback', + ].join('\n'); +} + +function renderSession(session: AgentPromptSessionContext | undefined): string { + if (!session?.id && !session?.label) return 'unrecorded'; + if (session.id && session.label) return `${session.label} (${session.id})`; + return session.id ?? session.label ?? 'unrecorded'; +} + +function renderPosture(posture: AgentPromptWorkspaceContext['posture']): string { + if (!posture) return 'unrecorded'; + const entries = Object.entries(posture).filter((entry): entry is [string, string] => + Boolean(entry[1]?.trim()), + ); + return entries.length > 0 ? entries.map(([key, value]) => `${key}=${value}`).join('; ') : 'unrecorded'; +} diff --git a/src/agents/contexts/graph.test.ts b/src/agents/contexts/graph.test.ts new file mode 100644 index 00000000..b7cde30a --- /dev/null +++ b/src/agents/contexts/graph.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest'; + +import type { GraphOverview } from '../../graph/snapshot.js'; +import { renderGraphContext } from './graph.js'; + +const overview: GraphOverview = { + lsn: 7, + nodeCount: 4, + edgeCount: 2, + nodes: [ + node(1, 'intent', 'goal', 'Fast local specification'), + node(2, 'design', 'module', 'Prompt composer'), + node(3, 'oracle', 'check', 'Prompt posture fixture'), + node(4, 'intent', 'constraint', 'No ambient Pi discovery'), + ], + edges: [ + { + id: 10, + specId: 1, + category: 'realization', + sourceId: 2, + targetId: 1, + basis: 'explicit', + createdAtLsn: 6, + updatedAtLsn: 6, + }, + { + id: 11, + specId: 1, + category: 'proof', + sourceId: 3, + targetId: 4, + stance: 'for', + basis: 'explicit', + createdAtLsn: 7, + updatedAtLsn: 7, + }, + ], +}; + +describe('renderGraphContext', () => { + it('renders the same selected-spec overview with lens-specific emphasis', () => { + const intent = renderGraphContext(overview, { lens: 'intent' }); + const design = renderGraphContext(overview, { lens: 'design' }); + const oracle = renderGraphContext(overview, { lens: 'oracle' }); + + expect(intent).toContain('[Selected-spec graph context · intent lens]'); + expect(design).toContain('[Selected-spec graph context · design lens]'); + expect(oracle).toContain('[Selected-spec graph context · oracle lens]'); + expect(intent).toContain('intent claims, terms, assumptions'); + expect(design).toContain('design modules/interfaces'); + expect(oracle).toContain('verification checks, evidence'); + expect(intent.indexOf('intent/goal')).toBeLessThan(intent.indexOf('design/module')); + expect(design.indexOf('design/module')).toBeLessThan(design.indexOf('intent/goal')); + expect(oracle.indexOf('oracle/check')).toBeLessThan(oracle.indexOf('intent/goal')); + expect(overview.nodes[0]?.title).toBe('Fast local specification'); + }); + + it('bounds rendered node and edge output', () => { + const rendered = renderGraphContext(overview, { lens: 'intent', maxNodes: 2, maxEdges: 1 }); + + expect(rendered).toContain('…2 more node(s) omitted'); + expect(rendered).toContain('…1 more edge(s) omitted'); + }); +}); + +function node( + id: number, + plane: GraphOverview['nodes'][number]['plane'], + kind: GraphOverview['nodes'][number]['kind'], + title: string, +): GraphOverview['nodes'][number] { + return { + id, + specId: 1, + plane, + kind, + title, + basis: 'explicit', + createdAtLsn: id, + updatedAtLsn: id, + }; +} diff --git a/src/agents/contexts/graph.ts b/src/agents/contexts/graph.ts new file mode 100644 index 00000000..a4158fb7 --- /dev/null +++ b/src/agents/contexts/graph.ts @@ -0,0 +1,83 @@ +import type { GraphNode } from '../../graph/schema/nodes.js'; +import type { GraphOverview } from '../../graph/snapshot.js'; +import type { AgentLensSelection } from '../../session/runtime-state.js'; + +export interface RenderGraphContextOptions { + readonly lens: AgentLensSelection; + readonly maxNodes?: number; + readonly maxEdges?: number; +} + +const DEFAULT_MAX_NODES = 8; +const DEFAULT_MAX_EDGES = 8; + +export function renderGraphContext(overview: GraphOverview, options: RenderGraphContextOptions): string { + const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES; + const maxEdges = options.maxEdges ?? DEFAULT_MAX_EDGES; + const emphasizedNodes = [...overview.nodes].sort((a, b) => { + const byLens = lensScore(b, options.lens) - lensScore(a, options.lens); + return byLens || a.id - b.id; + }); + + const lines = [ + `[Selected-spec graph context · ${options.lens} lens]`, + `- lsn: ${overview.lsn}; nodes: ${overview.nodeCount}; edges: ${overview.edgeCount}`, + `- emphasis: ${lensEmphasis(options.lens)}`, + ]; + + if (overview.nodeCount === 0) { + lines.push('- graph: empty'); + return lines.join('\n'); + } + + lines.push('- emphasized nodes:'); + for (const node of emphasizedNodes.slice(0, maxNodes)) { + lines.push(` - ${formatNode(node)}`); + } + if (overview.nodes.length > maxNodes) { + lines.push(` - …${overview.nodes.length - maxNodes} more node(s) omitted`); + } + + if (overview.edges.length > 0) { + lines.push('- edges:'); + for (const edge of overview.edges.slice(0, maxEdges)) { + const stance = edge.stance ? `/${edge.stance}` : ''; + lines.push(` - #${edge.sourceId} -[${edge.category}${stance}]-> #${edge.targetId}`); + } + if (overview.edges.length > maxEdges) { + lines.push(` - …${overview.edges.length - maxEdges} more edge(s) omitted`); + } + } + + return lines.join('\n'); +} + +function lensScore(node: GraphNode, lens: AgentLensSelection): number { + if (node.plane === lens) return 4; + if (lens === 'intent' && node.plane === 'plan') return 1; + if (lens === 'design' && (node.plane === 'intent' || node.plane === 'plan')) return 1; + if (lens === 'oracle' && node.kind === 'invariant') return 2; + return 0; +} + +function lensEmphasis(lens: AgentLensSelection): string { + switch (lens) { + case 'intent': + return 'intent claims, terms, assumptions, constraints, and decisions first'; + case 'design': + return 'design modules/interfaces and boundary implications first'; + case 'oracle': + return 'verification checks, evidence, obligations, and proof gaps first'; + case 'auto': + return 'AUTO lens selection pending; keep intent, design, and oracle cues visible'; + } +} + +function formatNode(node: GraphNode): string { + const body = node.body ? ` — ${truncate(node.body, 120)}` : ''; + return `[#${node.id}] ${node.plane}/${node.kind}: ${node.title}${body}`; +} + +function truncate(value: string, maxLength: number): string { + return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`; +} diff --git a/src/agents/contexts/index.ts b/src/agents/contexts/index.ts new file mode 100644 index 00000000..03fba947 --- /dev/null +++ b/src/agents/contexts/index.ts @@ -0,0 +1,3 @@ +export { renderCwdContext, type AgentPromptSessionContext, type RenderCwdContextInput } from './cwd.js'; +export { renderGraphContext, type RenderGraphContextOptions } from './graph.js'; +export { renderNodeContext, type RenderNodeContextOptions } from './node.js'; diff --git a/src/agents/contexts/node.test.ts b/src/agents/contexts/node.test.ts new file mode 100644 index 00000000..141b3153 --- /dev/null +++ b/src/agents/contexts/node.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; + +import type { GraphNode } from '../../graph/schema/nodes.js'; +import type { NeighborhoodResult } from '../../graph/snapshot.js'; +import { renderNodeContext } from './node.js'; + +const neighborhood: NeighborhoodResult = { + status: 'success', + anchor: node( + 1, + 'intent', + 'requirement', + 'Selected spec has graph truth', + 'A long body explains the requirement.', + ), + neighbors: [ + node(2, 'design', 'module', 'Graph snapshot reader'), + node(3, 'oracle', 'check', 'Prompt path test'), + ], + edges: [ + { + id: 5, + specId: 1, + category: 'realization', + sourceId: 2, + targetId: 1, + basis: 'explicit', + rationale: 'The reader supplies typed selected-spec data to context renderers.', + createdAtLsn: 5, + updatedAtLsn: 5, + }, + ], +}; + +describe('renderNodeContext', () => { + it('renders anchor, neighbors, and relevant edges with bounded output', () => { + const rendered = renderNodeContext(neighborhood, { maxNeighbors: 1, maxEdges: 1 }); + + expect(rendered).toContain('[Selected-spec node context]'); + expect(rendered).toContain('- anchor: [#1] intent/requirement: Selected spec has graph truth'); + expect(rendered).toContain('- anchor body: A long body explains the requirement.'); + expect(rendered).toContain('[#2] design/module: Graph snapshot reader'); + expect(rendered).toContain('…1 more neighbor(s) omitted'); + expect(rendered).toContain('#5: #2 -[realization]-> #1'); + }); + + it('renders a clear selected-spec missing-node result', () => { + expect(renderNodeContext({ status: 'not_found' })).toBe( + '[Selected-spec node context]\n- node: not found in selected spec', + ); + }); +}); + +function node( + id: number, + plane: GraphNode['plane'], + kind: GraphNode['kind'], + title: string, + body?: string, +): GraphNode { + return { + id, + specId: 1, + plane, + kind, + title, + ...(body ? { body } : {}), + basis: 'explicit', + createdAtLsn: id, + updatedAtLsn: id, + }; +} diff --git a/src/agents/contexts/node.ts b/src/agents/contexts/node.ts new file mode 100644 index 00000000..a17d19a8 --- /dev/null +++ b/src/agents/contexts/node.ts @@ -0,0 +1,63 @@ +import type { NeighborhoodResult } from '../../graph/snapshot.js'; + +export interface RenderNodeContextOptions { + readonly maxNeighbors?: number; + readonly maxEdges?: number; +} + +const DEFAULT_MAX_NEIGHBORS = 6; +const DEFAULT_MAX_EDGES = 8; + +export function renderNodeContext( + result: NeighborhoodResult, + options: RenderNodeContextOptions = {}, +): string { + if (result.status === 'not_found') { + return '[Selected-spec node context]\n- node: not found in selected spec'; + } + + const maxNeighbors = options.maxNeighbors ?? DEFAULT_MAX_NEIGHBORS; + const maxEdges = options.maxEdges ?? DEFAULT_MAX_EDGES; + const lines = [ + '[Selected-spec node context]', + `- anchor: [#${result.anchor.id}] ${result.anchor.plane}/${result.anchor.kind}: ${result.anchor.title}`, + ]; + + if (result.anchor.body) { + lines.push(`- anchor body: ${truncate(result.anchor.body, 180)}`); + } + + if (result.neighbors.length === 0) { + lines.push('- neighbors: none within requested hops'); + } else { + lines.push('- neighbors:'); + for (const neighbor of result.neighbors.slice(0, maxNeighbors)) { + lines.push(` - [#${neighbor.id}] ${neighbor.plane}/${neighbor.kind}: ${neighbor.title}`); + } + if (result.neighbors.length > maxNeighbors) { + lines.push(` - …${result.neighbors.length - maxNeighbors} more neighbor(s) omitted`); + } + } + + if (result.edges.length === 0) { + lines.push('- edges: none'); + } else { + lines.push('- edges:'); + for (const edge of result.edges.slice(0, maxEdges)) { + const stance = edge.stance ? `/${edge.stance}` : ''; + const rationale = edge.rationale ? ` — ${truncate(edge.rationale, 100)}` : ''; + lines.push( + ` - #${edge.id}: #${edge.sourceId} -[${edge.category}${stance}]-> #${edge.targetId}${rationale}`, + ); + } + if (result.edges.length > maxEdges) { + lines.push(` - …${result.edges.length - maxEdges} more edge(s) omitted`); + } + } + + return lines.join('\n'); +} + +function truncate(value: string, maxLength: number): string { + return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`; +} diff --git a/src/agents/definitions/elicitor.md b/src/agents/definitions/elicitor.md new file mode 100644 index 00000000..b4a9ff5c --- /dev/null +++ b/src/agents/definitions/elicitor.md @@ -0,0 +1,5 @@ +# Agent: elicitor + +The elicitor is the foreground Brunch session agent for elicit mode. It drives assistant-first structured exchanges, helps the human clarify the selected spec, and uses only resources advertised in the current prompt manifest. + +It should keep multi-spec discipline: every question, snapshot, proposal, and graph write targets the selected spec. diff --git a/src/agents/definitions/reviewer.md b/src/agents/definitions/reviewer.md new file mode 100644 index 00000000..b6d28b02 --- /dev/null +++ b/src/agents/definitions/reviewer.md @@ -0,0 +1,5 @@ +# Agent: reviewer + +The reviewer is a future side agent for checking proposals and commitments. It is not the foreground session agent in this slice. + +Reviewer resources exist so manifests can point at Brunch-owned definitions when later review-cycle work makes them active. diff --git a/src/agents/goals/capture-posture.md b/src/agents/goals/capture-posture.md new file mode 100644 index 00000000..1706248c --- /dev/null +++ b/src/agents/goals/capture-posture.md @@ -0,0 +1,5 @@ +# Goal: capture-posture + +Confirm workspace posture that affects how Brunch should work: certainty, stakes, audience, horizon, migration, and sourcing posture. + +Posture is workspace-scoped product state. Do not write it as spec graph truth unless the user separately frames a claim about the product being specified. diff --git a/src/agents/goals/commit-converge.md b/src/agents/goals/commit-converge.md new file mode 100644 index 00000000..a30e610c --- /dev/null +++ b/src/agents/goals/commit-converge.md @@ -0,0 +1,5 @@ +# Goal: commit-converge + +Reduce open ambiguity into reviewable commitments. Use this only when the spec has enough readiness for commitment work. + +Prefer atomic review-set or graph-command paths. Do not partially commit a batch or silently downgrade rejected material into graph truth. diff --git a/src/agents/goals/elicit-expand.md b/src/agents/goals/elicit-expand.md new file mode 100644 index 00000000..ddccf0f7 --- /dev/null +++ b/src/agents/goals/elicit-expand.md @@ -0,0 +1,5 @@ +# Goal: elicit-expand + +Grow the selected spec while ambiguity is still useful. Surface options, tradeoffs, missing claims, and candidate relationships without forcing premature closure. + +Keep new material tied to the selected spec. If a claim is low-confidence, ask or mark it as uncertain rather than committing it as graph truth. diff --git a/src/agents/goals/grounding-advance.md b/src/agents/goals/grounding-advance.md new file mode 100644 index 00000000..a570e00f --- /dev/null +++ b/src/agents/goals/grounding-advance.md @@ -0,0 +1,5 @@ +# Goal: grounding-advance + +Establish the selected spec's basic frame: what it is, who it is for, what problem it answers, and what value would make it worth continuing. + +Stay elicitation-first. Prefer one structured question or contrast at a time. Do not claim the grade is ready to advance without concrete grounding evidence. diff --git a/src/agents/index.ts b/src/agents/index.ts new file mode 100644 index 00000000..1d5ce9b1 --- /dev/null +++ b/src/agents/index.ts @@ -0,0 +1,30 @@ +export { + composeAgentPrompt, + type AgentPromptSpecContext, + type AgentPromptSnapshotContext, + type AgentPromptWorkspaceContext, + type ComposeAgentPromptInput, + type ComposeAgentPromptResult, +} from './compose.js'; +export { + AGENT_PROMPT_DEFINITIONS, + GOAL_RESOURCES, + LENS_RESOURCES, + METHOD_RESOURCES, + STRATEGY_RESOURCES, + manifestsForState, + type AgentPromptDefinition, + type MethodId, + type PromptManifests, + type PromptResourceManifestEntry, + type ReadinessGrade, +} from './state.js'; +export { + renderCwdContext, + renderGraphContext, + renderNodeContext, + type AgentPromptSessionContext, + type RenderCwdContextInput, + type RenderGraphContextOptions, + type RenderNodeContextOptions, +} from './contexts/index.js'; diff --git a/src/agents/lenses/design.md b/src/agents/lenses/design.md new file mode 100644 index 00000000..7b011bd6 --- /dev/null +++ b/src/agents/lenses/design.md @@ -0,0 +1,5 @@ +# Lens: design + +Focus on modules, interfaces, dependency direction, ownership, and boundary pressure. + +Design observations can inform the spec, but do not overfit implementation structure into user intent. When a boundary choice is durable, make the decision explicit. diff --git a/src/agents/lenses/intent.md b/src/agents/lenses/intent.md new file mode 100644 index 00000000..09f26153 --- /dev/null +++ b/src/agents/lenses/intent.md @@ -0,0 +1,5 @@ +# Lens: intent + +Focus on intent-plane claims: goals, thesis, terms, context, requirements, assumptions, constraints, invariants, decisions, criteria, and examples. + +Ask what claim would become clearer or safer if captured. Avoid turning design or oracle observations into intent claims unless the user frames them that way. diff --git a/src/agents/lenses/oracle.md b/src/agents/lenses/oracle.md new file mode 100644 index 00000000..395f35f4 --- /dev/null +++ b/src/agents/lenses/oracle.md @@ -0,0 +1,5 @@ +# Lens: oracle + +Focus on proof obligations, checks, validation methods, evidence, and blind spots. + +Prefer observable obligations over generic test labels. Name what a check would prove and what rival behavior it would fail to rule out. diff --git a/src/agents/methods/commit-graph.md b/src/agents/methods/commit-graph.md new file mode 100644 index 00000000..62d11be5 --- /dev/null +++ b/src/agents/methods/commit-graph.md @@ -0,0 +1,5 @@ +# Method: commit-graph + +Commit graph truth only through Brunch graph tools backed by CommandExecutor. Treat `structural_illegal`, `policy_blocked`, and `version_conflict` results as meaningful diagnostics. + +Do not mutate storage directly. Do not split one conceptual batch into hidden partial writes. diff --git a/src/.pi/context/prompt-packs/candidate-proposals.md b/src/agents/methods/generate-proposal.md similarity index 59% rename from src/.pi/context/prompt-packs/candidate-proposals.md rename to src/agents/methods/generate-proposal.md index e5e9f78d..cf321783 100644 --- a/src/.pi/context/prompt-packs/candidate-proposals.md +++ b/src/agents/methods/generate-proposal.md @@ -1,6 +1,12 @@ -# Candidate proposals +# Method: generate-proposal -- Internally reason using the D31-L meta-rubric axes: `legibility_cost_of_knowing`, `failure_modes`, `coverage_range`, and `commitment`. +Generate proposal material as a coherent batch with explicit claims, edges, and rationale. Keep proposed material separate from accepted graph truth. + +Before presenting a review set, ensure it is structurally valid enough for the user to review rather than debug. + +Candidate-proposal constraints: + +- Internally reason with the D31-L meta-rubric axes: `legibility_cost_of_knowing`, `failure_modes`, `coverage_range`, and `commitment`. - Derive user-facing `present_candidates` fields: `core_bet`, `best_fit`, `cost_complexity`, `covers_well`, `main_risks`, `lock_in_constraints`, and optional `recommendation`. - `core_bet` is the candidate headline or thesis. - Avoid fake low/medium/high scalar ratings for cost, risk, confidence, timeline, or verification. diff --git a/src/agents/methods/infer-and-capture.md b/src/agents/methods/infer-and-capture.md new file mode 100644 index 00000000..96b158d6 --- /dev/null +++ b/src/agents/methods/infer-and-capture.md @@ -0,0 +1,12 @@ +# Method: infer-and-capture + +After an exchange, extract only high-confidence facts that are directly supported by the transcript. Low-confidence implications become follow-up questions or notes, not graph truth. + +Capture must target the selected spec and route through Brunch-owned mutation surfaces. + +Capture-analysis constraints: + +- `capture_*` follows `request_*`; capture is transcript-native analysis, not graph mutation, and analyzes a completed exchange rather than creating graph truth by itself. +- For candidate selection, consume the selected candidate `user_rubric`, selected candidate `meta_rubric`, selected candidate `graph_refs`, and the user's `comment` when present. +- Do not invent final graph payloads, LSNs, or `CommandExecutor` result shapes in capture analysis. +- Future graph writes must route through `CommandExecutor`; capture analysis must never imply a graph bypass. diff --git a/src/agents/methods/read-snapshot.md b/src/agents/methods/read-snapshot.md new file mode 100644 index 00000000..113639fe --- /dev/null +++ b/src/agents/methods/read-snapshot.md @@ -0,0 +1,5 @@ +# Method: read-snapshot + +Use pushed context handles first. When detail matters, call the relevant snapshot/read tool for selected-spec graph or node context. + +Snapshots are read-only context. Do not treat a snapshot as mutation authority or workspace-global truth. diff --git a/src/agents/methods/review-for-gaps.md b/src/agents/methods/review-for-gaps.md new file mode 100644 index 00000000..ff9d9633 --- /dev/null +++ b/src/agents/methods/review-for-gaps.md @@ -0,0 +1,5 @@ +# Method: review-for-gaps + +Review the current commitment or proposal for missing evidence, contradictions, weak edges, and unresolved decisions. + +Name the gap and the consequence. Do not invent a broad review framework when one concrete missing proof or claim would unblock the next turn. diff --git a/src/agents/methods/run-structured-exchange.md b/src/agents/methods/run-structured-exchange.md new file mode 100644 index 00000000..06f10f9f --- /dev/null +++ b/src/agents/methods/run-structured-exchange.md @@ -0,0 +1,15 @@ +# Method: run-structured-exchange + +Use Brunch structured exchanges for typed human responses: questions, single-choice, multi-choice, freeform, and review outcomes. + +Each exchange should have a clear reason, a compact prompt, and response options that map to the current goal. Do not rely on ambient chat when a typed exchange is needed. + +Transcript/projection rules: + +- Structured exchanges are transcript-native `present_* -> request_* -> capture_*` tool result families. +- `toolResult.content` is durable markdown for transcript display and model-readable context. +- `toolResult.details` is structured recovery/projection data; classify rows by `details.schema` plus `v`, not by tool name alone. +- `renderCall` is display-only and must not carry durable Brunch meaning. +- Use `tool_meta` for sequence/sibling facts. +- Use `comment` for user-authored text and `message` for system/runtime-authored text. +- Request outcomes are an exactly-one property-presence union: `answered`, `cancelled`, or `unavailable`. diff --git a/src/agents/state.test.ts b/src/agents/state.test.ts new file mode 100644 index 00000000..dee96531 --- /dev/null +++ b/src/agents/state.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; + +import { projectBrunchAgentState } from '../session/runtime-state.js'; +import { activeToolNamesForPosture, manifestsForState } from './state.js'; + +const registeredToolNames = [ + 'read', + 'grep', + 'find', + 'ls', + 'bash', + 'edit', + 'write', + 'present_question', + 'present_options', + 'read_graph', + 'commit_graph', +]; + +describe('agent posture policy', () => { + it('derives method manifests and active tool names from one grade policy', () => { + const state = projectBrunchAgentState([]); + + const groundingMethods = manifestsForState(state, 'grounding_onboarding').methods.map( + (entry) => entry.name, + ); + const groundingTools = activeToolNamesForPosture({ + registeredToolNames, + state, + readinessGrade: 'grounding_onboarding', + }); + const elicitationMethods = manifestsForState(state, 'elicitation_ready').methods.map( + (entry) => entry.name, + ); + const elicitationTools = activeToolNamesForPosture({ + registeredToolNames, + state, + readinessGrade: 'elicitation_ready', + }); + const commitmentsMethods = manifestsForState(state, 'commitments_ready').methods.map( + (entry) => entry.name, + ); + const commitmentsTools = activeToolNamesForPosture({ + registeredToolNames, + state, + readinessGrade: 'commitments_ready', + }); + + expect(groundingMethods).not.toContain('commit-graph'); + expect(groundingTools).not.toContain('commit_graph'); + expect(groundingTools).toContain('read_graph'); + expect(groundingTools).not.toContain('bash'); + + expect(elicitationMethods).toContain('commit-graph'); + expect(elicitationTools).toContain('commit_graph'); + expect(commitmentsMethods).toContain('generate-proposal'); + expect(commitmentsTools).toContain('commit_graph'); + }); +}); diff --git a/src/agents/state.ts b/src/agents/state.ts new file mode 100644 index 00000000..40f6ffe7 --- /dev/null +++ b/src/agents/state.ts @@ -0,0 +1,338 @@ +import { fileURLToPath } from 'node:url'; + +import type { ReadinessGrade } from '../graph/index.js'; +import type { + AgentGoalId, + AgentLensId, + AgentRoleId, + AgentStrategyId, + ResolvedBrunchAgentState, +} from '../session/runtime-state.js'; + +export type { ReadinessGrade }; +export type PromptResourceFamily = 'goals' | 'strategies' | 'lenses' | 'methods' | 'definitions'; +export type MethodId = + | 'run-structured-exchange' + | 'infer-and-capture' + | 'commit-graph' + | 'read-snapshot' + | 'generate-proposal' + | 'review-for-gaps'; + +export interface PromptResourceManifestEntry { + name: string; + description: string; + location: string; +} + +export interface AgentPromptDefinition { + id: AgentRoleId; + description: string; + model: string; + thinking: 'low' | 'medium' | 'high'; + toolAuthority: string; + allowedGoals: readonly AgentGoalId[]; + allowedStrategies: readonly AgentStrategyId[]; + allowedLenses: readonly AgentLensId[]; + allowedMethods: readonly MethodId[]; +} + +export interface PromptManifests { + goals: readonly PromptResourceManifestEntry[]; + strategies: readonly PromptResourceManifestEntry[]; + lenses: readonly PromptResourceManifestEntry[]; + methods: readonly PromptResourceManifestEntry[]; +} + +export interface BrunchPostureToolPolicyInput { + registeredToolNames: readonly string[]; + state: ResolvedBrunchAgentState; + readinessGrade: ReadinessGrade; +} + +const GRADE_RANK: Record = { + grounding_onboarding: 0, + elicitation_ready: 1, + commitments_ready: 2, + planning_ready: 3, +}; + +const GOAL_MIN_GRADE: Record = { + 'grounding-advance': 'grounding_onboarding', + 'elicit-expand': 'elicitation_ready', + 'commit-converge': 'commitments_ready', + 'capture-posture': 'grounding_onboarding', +}; + +const STRATEGY_MIN_GRADE: Record = { + 'step-wise-decision-tree': 'grounding_onboarding', + 'step-wise-disambiguate': 'grounding_onboarding', + 'propose-graph': 'elicitation_ready', + 'project-graph': 'commitments_ready', +}; + +const LENS_MIN_GRADE: Record = { + intent: 'grounding_onboarding', + design: 'elicitation_ready', + oracle: 'elicitation_ready', +}; + +const METHOD_MIN_GRADE: Record = { + 'run-structured-exchange': 'grounding_onboarding', + 'infer-and-capture': 'grounding_onboarding', + 'read-snapshot': 'grounding_onboarding', + 'commit-graph': 'elicitation_ready', + 'generate-proposal': 'commitments_ready', + 'review-for-gaps': 'commitments_ready', +}; + +const METHOD_TOOL_NAMES: Partial> = { + 'run-structured-exchange': ['present_question', 'present_options'], + 'read-snapshot': ['read_graph'], + 'commit-graph': ['commit_graph'], +}; + +const ELICIT_BASE_TOOL_NAMES = ['read', 'grep', 'find', 'ls'] as const; +const ELICIT_BLOCKED_TOOL_NAMES = ['bash', 'edit', 'write'] as const; + +export const AGENT_PROMPT_DEFINITIONS: Record = { + elicitor: { + id: 'elicitor', + description: + 'Foreground Brunch session agent that elicits, disambiguates, and captures selected-spec intent.', + model: 'default', + thinking: 'medium', + toolAuthority: + 'elicit read-only; graph writes only through Brunch graph tools when a legal strategy allows them', + allowedGoals: ['grounding-advance', 'elicit-expand', 'commit-converge', 'capture-posture'], + allowedStrategies: [ + 'step-wise-decision-tree', + 'step-wise-disambiguate', + 'propose-graph', + 'project-graph', + ], + allowedLenses: ['intent', 'design', 'oracle'], + allowedMethods: [ + 'run-structured-exchange', + 'infer-and-capture', + 'commit-graph', + 'read-snapshot', + 'generate-proposal', + 'review-for-gaps', + ], + }, +}; + +export const GOAL_RESOURCES: Record = { + 'grounding-advance': resource( + 'goals', + 'grounding-advance', + 'Establish the basic initiative frame and readiness evidence for moving beyond onboarding.', + ), + 'elicit-expand': resource( + 'goals', + 'elicit-expand', + 'Expand the selected spec while ambiguity remains productive.', + ), + 'commit-converge': resource( + 'goals', + 'commit-converge', + 'Converge on reviewable commitments once the spec is ready for commitments.', + ), + 'capture-posture': resource( + 'goals', + 'capture-posture', + 'Confirm workspace posture without storing it as spec or graph truth.', + ), +}; + +export const STRATEGY_RESOURCES: Record = { + 'step-wise-decision-tree': resource( + 'strategies', + 'step-wise-decision-tree', + 'Ask one structured question at a time and branch from the answer.', + ), + 'step-wise-disambiguate': resource( + 'strategies', + 'step-wise-disambiguate', + 'Use contrastive examples to collapse meaningful ambiguity.', + ), + 'propose-graph': resource( + 'strategies', + 'propose-graph', + 'Offer a concept-level graph proposal and commit only through Brunch graph tools after acceptance.', + ), + 'project-graph': resource( + 'strategies', + 'project-graph', + 'Generate a dry-run-valid review-set proposal for user approval.', + ), +}; + +export const LENS_RESOURCES: Record = { + intent: resource( + 'lenses', + 'intent', + 'Focus on intent-plane claims: goals, terms, assumptions, constraints, and decisions.', + ), + design: resource('lenses', 'design', 'Focus on design implications and module/interface boundaries.'), + oracle: resource( + 'lenses', + 'oracle', + 'Focus on verification obligations, checks, evidence, and blind spots.', + ), +}; + +export const METHOD_RESOURCES: Record = { + 'run-structured-exchange': resource( + 'methods', + 'run-structured-exchange', + 'Present typed Brunch exchanges and request typed responses.', + ), + 'infer-and-capture': resource( + 'methods', + 'infer-and-capture', + 'Extract only high-confidence facts from a completed exchange.', + ), + 'commit-graph': resource( + 'methods', + 'commit-graph', + 'Commit graph truth only through Brunch graph tools and CommandExecutor-backed results.', + ), + 'read-snapshot': resource( + 'methods', + 'read-snapshot', + 'Use pushed context handles and snapshot tools for selected-spec context.', + ), + 'generate-proposal': resource( + 'methods', + 'generate-proposal', + 'Generate reviewable candidate graph material without committing it directly.', + ), + 'review-for-gaps': resource( + 'methods', + 'review-for-gaps', + 'Review commitments for gaps, conflicts, and verification debt.', + ), +}; + +export function manifestsForState( + state: ResolvedBrunchAgentState, + readinessGrade: ReadinessGrade, +): PromptManifests { + const definition = AGENT_PROMPT_DEFINITIONS[state.agentRole]; + if (!definition) { + throw new Error(`Unknown Brunch agent "${state.agentRole}".`); + } + if (definition.id !== state.agentRole || state.operationalMode !== 'elicit') { + throw new Error( + `Agent "${state.agentRole}" is not legal in operational mode "${state.operationalMode}".`, + ); + } + + return { + goals: selectAxisResources({ + label: 'goal', + selection: state.agentGoal, + allowed: definition.allowedGoals, + resources: GOAL_RESOURCES, + minGrades: GOAL_MIN_GRADE, + readinessGrade, + state, + }), + strategies: selectAxisResources({ + label: 'strategy', + selection: state.agentStrategy, + allowed: definition.allowedStrategies, + resources: STRATEGY_RESOURCES, + minGrades: STRATEGY_MIN_GRADE, + readinessGrade, + state, + }), + lenses: selectAxisResources({ + label: 'lens', + selection: state.agentLens, + allowed: definition.allowedLenses, + resources: LENS_RESOURCES, + minGrades: LENS_MIN_GRADE, + readinessGrade, + state, + }), + methods: methodIdsForState(state, readinessGrade).map((method) => METHOD_RESOURCES[method]), + }; +} + +export function methodIdsForState( + state: ResolvedBrunchAgentState, + readinessGrade: ReadinessGrade, +): readonly MethodId[] { + const definition = AGENT_PROMPT_DEFINITIONS[state.agentRole]; + if (!definition || definition.id !== state.agentRole || state.operationalMode !== 'elicit') return []; + return definition.allowedMethods.filter((method) => isGradeLegal(method, readinessGrade, METHOD_MIN_GRADE)); +} + +export function activeToolNamesForPosture({ + registeredToolNames, + state, + readinessGrade, +}: BrunchPostureToolPolicyInput): string[] { + if (state.operationalModeDefinition.toolPolicyId !== 'elicit-read-only') return []; + + const legalTools = new Set(ELICIT_BASE_TOOL_NAMES); + for (const method of methodIdsForState(state, readinessGrade)) { + for (const toolName of METHOD_TOOL_NAMES[method] ?? []) { + legalTools.add(toolName); + } + } + + const blockedTools = new Set(ELICIT_BLOCKED_TOOL_NAMES); + + return registeredToolNames.filter((toolName) => legalTools.has(toolName) && !blockedTools.has(toolName)); +} + +function selectAxisResources({ + label, + selection, + allowed, + resources, + minGrades, + readinessGrade, + state, +}: { + label: 'goal' | 'strategy' | 'lens'; + selection: 'auto' | TId; + allowed: readonly TId[]; + resources: Record; + minGrades: Record; + readinessGrade: ReadinessGrade; + state: ResolvedBrunchAgentState; +}): readonly PromptResourceManifestEntry[] { + const legal = allowed.filter((id) => isGradeLegal(id, readinessGrade, minGrades)); + if (selection === 'auto') return legal.map((id) => resources[id]); + if (!legal.includes(selection)) { + throw new Error( + `Pinned ${label} "${selection}" is not legal for ${state.agentRole} in ${state.operationalMode} at readiness grade ${readinessGrade}.`, + ); + } + return [resources[selection]]; +} + +function isGradeLegal( + id: TId, + readinessGrade: ReadinessGrade, + minGrades: Record, +): boolean { + return GRADE_RANK[readinessGrade] >= GRADE_RANK[minGrades[id]]; +} + +function resource( + family: PromptResourceFamily, + id: string, + description: string, +): PromptResourceManifestEntry { + return { + name: id, + description, + location: fileURLToPath(new URL(`./${family}/${id}.md`, import.meta.url)), + }; +} diff --git a/src/agents/strategies/project-graph.md b/src/agents/strategies/project-graph.md new file mode 100644 index 00000000..ea22f85e --- /dev/null +++ b/src/agents/strategies/project-graph.md @@ -0,0 +1,5 @@ +# Strategy: project-graph + +Generate a review-set proposal from established material. The proposal should be dry-run-valid before it reaches the user. + +Approval commits the whole batch atomically. Request-changes regenerates or narrows the proposal; rejection does not create graph truth. diff --git a/src/agents/strategies/propose-graph.md b/src/agents/strategies/propose-graph.md new file mode 100644 index 00000000..1ef645f8 --- /dev/null +++ b/src/agents/strategies/propose-graph.md @@ -0,0 +1,5 @@ +# Strategy: propose-graph + +Offer a concept-level graph proposal after enough context exists. The user accepts or rejects the concept; accepted concepts may be committed through Brunch graph tools. + +Never bypass CommandExecutor-backed graph tools. If tool diagnostics reject the batch, use the diagnostics to retry or explain the failure. diff --git a/src/agents/strategies/step-wise-decision-tree.md b/src/agents/strategies/step-wise-decision-tree.md new file mode 100644 index 00000000..6bdf2f40 --- /dev/null +++ b/src/agents/strategies/step-wise-decision-tree.md @@ -0,0 +1,5 @@ +# Strategy: step-wise-decision-tree + +Ask one structured question, wait for the answer, then choose the next branch. Keep the branch reason visible so the human can correct the direction. + +Use radio or checkbox choices when the options are known; use freeform when the next distinction is not yet enumerable. diff --git a/src/agents/strategies/step-wise-disambiguate.md b/src/agents/strategies/step-wise-disambiguate.md new file mode 100644 index 00000000..f0c90f78 --- /dev/null +++ b/src/agents/strategies/step-wise-disambiguate.md @@ -0,0 +1,5 @@ +# Strategy: step-wise-disambiguate + +Collapse ambiguity with contrastive examples. Present two or three plausible meanings and ask which is closer, what differs, or what should be combined. + +Use this when words or goals feel overloaded. Preserve the user's chosen distinction as the next working vocabulary. diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index dbc7236c..dc3db44a 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -13,8 +13,12 @@ import { describe, expect, it } from 'vitest'; import { createBrunchPiProfile } from './.pi/brunch-pi-profile.js'; import { - BRUNCH_WORKSPACE_COMMAND, - BRUNCH_WORKSPACE_SHORTCUT, + BRUNCH_CONTINUE_COMMAND, + BRUNCH_LENS_COMMAND, + BRUNCH_MODE_COMMAND, + BRUNCH_STRATEGY_COMMAND, + BRUNCH_SWITCH_COMMAND, + BRUNCH_SWITCH_SHORTCUT, chromeStateForWorkspace, createBrunchPiExtensionShell, registerBrunchAlternatives, @@ -425,7 +429,13 @@ describe('Brunch TUI boot', () => { (sessionManager) => { boundSessionIds.push(sessionManager.getSessionId()); }, - { coordinator: noOpWorkspaceCoordinator(cwd) }, + { + coordinator: noOpWorkspaceCoordinator(cwd), + promptContext: { + spec: { id: 1, name: 'Spec One', readinessGrade: 'grounding_onboarding' }, + workspace: { cwd }, + }, + }, )({ on: (event: string, handler: never) => { if (event === 'session_start') { @@ -493,20 +503,46 @@ describe('Brunch TUI boot', () => { 'request_choice', 'request_choices', ]); - expect(commands.get(BRUNCH_WORKSPACE_COMMAND)?.description).toBe('Open the Brunch spec/session picker'); + expect(commands.get(BRUNCH_SWITCH_COMMAND)?.description).toBe('Open the Brunch spec/session picker'); const retiredWorkspaceCommand = ['brunch', 'workspace'].join('-'); expect(commands.has(retiredWorkspaceCommand)).toBe(false); - expect(shortcuts.get(BRUNCH_WORKSPACE_SHORTCUT)?.description).toBe('Open the Brunch spec/session picker'); + expect(commands.has('brunch')).toBe(false); + for (const stubCommand of [ + BRUNCH_CONTINUE_COMMAND, + BRUNCH_LENS_COMMAND, + BRUNCH_STRATEGY_COMMAND, + BRUNCH_MODE_COMMAND, + ]) { + expect(commands.has(stubCommand)).toBe(true); + } + expect(shortcuts.get(BRUNCH_SWITCH_SHORTCUT)?.description).toBe('Open the Brunch spec/session picker'); expect(shortcuts.has('ctrl+b')).toBe(false); const shortcutEvents: string[] = []; - const shortcut = shortcuts.get(BRUNCH_WORKSPACE_SHORTCUT); + const shortcut = shortcuts.get(BRUNCH_SWITCH_SHORTCUT); expect(shortcut).toBeDefined(); const shortcutHandler = shortcut!.handler as (ctx: unknown) => Promise | void; await shortcutHandler({ ui: fakeUi((method, type) => shortcutEvents.push(`${method}:${type}`)), }); expect(shortcutEvents).toEqual(['notify:warning']); + + const stubEvents: string[] = []; + const stubCtx = { + ui: fakeUi((method, type) => stubEvents.push(`${method}:${type}`)), + }; + for (const stubCommand of [ + BRUNCH_CONTINUE_COMMAND, + BRUNCH_LENS_COMMAND, + BRUNCH_STRATEGY_COMMAND, + BRUNCH_MODE_COMMAND, + ]) { + const stub = commands.get(stubCommand); + expect(stub).toBeDefined(); + const stubHandler = stub!.handler as (args: string, ctx: unknown) => Promise | void; + await stubHandler('', stubCtx); + } + expect(stubEvents).toEqual(['notify:info', 'notify:info', 'notify:info', 'notify:info']); }); it('opens the spec/session picker from the Brunch command', async () => { @@ -875,19 +911,7 @@ 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', - 'request_answer', - 'request_choice', - 'request_choices', - ], - ]); + expect(activeTools).toEqual([['read', 'grep', 'find', 'ls', 'present_question', 'present_options']]); await expect( Promise.resolve(events.before_agent_start?.({ systemPrompt: 'base' } as never)), ).resolves.toBeUndefined(); diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index c4d25c78..8988b208 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -138,29 +138,51 @@ export function createBrunchAgentSessionRuntimeFactory({ productUpdates, }: BrunchTuiLaunchContext): CreateAgentSessionRuntimeFactory { return async ({ cwd, agentDir: runtimeAgentDir, sessionManager }) => { - const currentWorkspace = await coordinator.bindCurrentSpecToReplacementSession(sessionManager); + let currentWorkspace = await coordinator.bindCurrentSpecToReplacementSession(sessionManager); const graph = await openWorkspaceGraphRuntime(cwd); - // Bind graph snapshot readers to the coordinator's current spec (D61-L). - // The same runtime factory can be reused after /brunch switches sessions, - // so never close over the spec that happened to launch the factory. - const specId = currentWorkspace.spec.id; const graphDeps = { - specId, + get specId() { + return currentWorkspace.spec.id; + }, commandExecutor: graph.commandExecutor, - snapshots: graph.forSpec(specId), + snapshots: { + getGraphOverview: () => graph.forSpec(currentWorkspace.spec.id).getGraphOverview(), + getNodeNeighborhood: (nodeId: number, options?: { hops?: number }) => + graph.forSpec(currentWorkspace.spec.id).getNodeNeighborhood(nodeId, options), + }, ...(productUpdates ? { productUpdates } : {}), }; + const bindCurrentWorkspace = async (replacementSessionManager: typeof sessionManager) => { + currentWorkspace = await coordinator.bindCurrentSpecToReplacementSession(replacementSessionManager); + }; const profile = createBrunchPiProfile({ cwd, agentDir: runtimeAgentDir, extensionFactories: [ - createBrunchPiExtensionShell( - chromeStateForWorkspace(currentWorkspace), - async (replacementSessionManager) => { - await coordinator.bindCurrentSpecToReplacementSession(replacementSessionManager); + createBrunchPiExtensionShell(chromeStateForWorkspace(currentWorkspace), bindCurrentWorkspace, { + coordinator, + graph: graphDeps, + promptContext: () => { + const specId = currentWorkspace.spec.id; + const selectedSpec = graph.commandExecutor.getSpec(specId); + if (!selectedSpec) { + throw new Error(`No selected spec found for Brunch prompt context: ${specId}`); + } + return { + spec: { + id: selectedSpec.id, + name: selectedSpec.name, + readinessGrade: selectedSpec.readinessGrade, + }, + workspace: { cwd }, + session: { + id: currentWorkspace.session.id, + ...(currentWorkspace.session.name ? { label: currentWorkspace.session.name } : {}), + }, + graphSnapshots: graphDeps.snapshots, + }; }, - { coordinator, graph: graphDeps }, - ), + }), ], }); const services = await createAgentSessionServices({ diff --git a/src/print-snapshot.test.ts b/src/print-snapshot.test.ts index dd4c8e30..5cd0debe 100644 --- a/src/print-snapshot.test.ts +++ b/src/print-snapshot.test.ts @@ -1,3 +1,4 @@ +import type { SessionManager } from '@earendil-works/pi-coding-agent'; import { describe, expect, it } from 'vitest'; import { renderWorkspaceSnapshot, workspaceSnapshotFromState } from './print-snapshot.js'; @@ -13,7 +14,7 @@ function readyState(): WorkspaceSessionState { session: { id: 'session-1', file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', - manager: {} as WorkspaceSessionState & never, + manager: {} as SessionManager, }, chrome: { cwd, diff --git a/src/rpc/handlers.test.ts b/src/rpc/handlers.test.ts index 4e2498e7..a38b7ace 100644 --- a/src/rpc/handlers.test.ts +++ b/src/rpc/handlers.test.ts @@ -1541,6 +1541,19 @@ describe('JSON-RPC handlers', () => { }, cwd, }); + + await expect( + handlers.handle({ + jsonrpc: '2.0', + id: 10, + method: 'session.exchanges', + params: { sessionId: workspace.session.id }, + }), + ).resolves.toMatchObject({ + jsonrpc: '2.0', + id: 10, + result: { status: 'ready' }, + }); }); it('serves runtime state by explicit spec and session id without opening selected session', async () => { diff --git a/src/session/workspace-session-coordinator.ts b/src/session/workspace-session-coordinator.ts index 3845e587..1d9dddec 100644 --- a/src/session/workspace-session-coordinator.ts +++ b/src/session/workspace-session-coordinator.ts @@ -31,7 +31,7 @@ interface WorkspaceProjectState { slug: string; } -interface WorkspacePostureState { +export interface WorkspacePostureState { certainty: string; stakes: string; audience: string; diff --git a/src/web/app.test.tsx b/src/web/app.test.tsx index bc5610d7..a2c944f8 100644 --- a/src/web/app.test.tsx +++ b/src/web/app.test.tsx @@ -95,6 +95,7 @@ function rpcClient(options?: { graphOverview?: typeof emptyGraphOverview | typeof populatedGraphOverview; calls?: RpcCall[]; listeners?: Set; + close?: ReturnType; }): WebSocketRpcClient { const snapshot = options?.snapshot ?? readySnapshot; const calls = options?.calls; @@ -117,7 +118,7 @@ function rpcClient(options?: { listeners.add(listener); return () => listeners.delete(listener); }, - close: vi.fn(), + close: options?.close ?? vi.fn(), } as unknown as WebSocketRpcClient; } @@ -259,11 +260,12 @@ describe('Brunch React web app', () => { }); it('disposes the root-owned RPC client', () => { - const client = rpcClient(); + const close = vi.fn(); + const client = rpcClient({ close }); const runtime = createBrunchWebRuntime({ rpcClient: client }); runtime.dispose(); - expect(client.close).toHaveBeenCalledOnce(); + expect(close).toHaveBeenCalledOnce(); }); });