fix(effort-viz): make primitive and lifecycle legible at a glance - #228
Conversation
…t name Journals the owner call through the typed effort-graph mutations rather than hand-edited frontmatter: - Finding (dead-end) recording why the crumb naming line does not hold: crumb reads as leftovers, collides with breadcrumb navigation, and hides the Effort primitive that the API, CLI, and skills already teach. Both prior Decisions listed exactly these as reversal criteria. - Decision "Keep Effort Graph as the product name", superseding Crumb Trail (and transitively Crumb Graph), accepted. - Both rename Issues resolved as wontfix, resolved_by the new Decision - the work is cancelled, not postponed. Change-Id: Id0da8e7252e90b4fee9fac2ea10b307fbe5943c8
… canvas The graph could not answer its most basic question: what kind of record is this? `nodeColor` hashed hue from the *effortId* and gave node kind only a +/-0.06 lightness nudge, so every record in an Effort cluster rendered as the same coloured circle. A per-kind palette already existed in `RelationLegend.tsx` and was dead code; the legend drew its six swatches from the broken function, so it showed six identical dots next to six different labels. Encoding - Hue now carries the primitive. Effort membership was already delivered by three channels - the force layout's shared centroid, a large labelled hub, and hub-tinted membership spokes - so it was spending the strongest nominal channel on the attribute that needed it least. - Shape repeats hue as a colour-vision backstop: amber/red and blue/violet partially merge under deuteranopia. Outlines are area-normalized in `lib/glyphs.ts` so a triangle and a square read at equal visual weight; otherwise size would imply a ranking nobody intended. - Effort hubs became neutral rings with a cluster-tinted core. The core also covers the membership spokes converging on the centre, which previously showed through the ring's hole as clutter. - Legend swatches and canvas geometry now derive from the same outlines and palette, so the key cannot drift from the render, and the legend lists only the relations the current generation contains. Lifecycle - `lib/lifecycle.ts` derives effective state from edges. A superseded Decision keeps `state: accepted` in frontmatter, so reading the field labelled retired reasoning as committed - the worst possible error on the record a reader most needs to get right. Retired records now fade with a struck-through label, and the drawer explains why its frontmatter disagrees. - Badges are keyed on `primitive:state`: a Risk's "accepted" means "we chose to live with this hazard" and must not borrow a Decision's reassuring green. Added the missing wontfix, mitigated, realized, and Effort status vocabulary. - Open blocker Issues wear an amber warning outline. Layout and camera - Added cluster separation to the force model. Generic node repulsion separates records but lets clusters interleave, and position is the channel carrying cluster identity. Cluster aggregates moved onto the scratch object, removing three map allocations per frame. - Effort titles anchor above their cluster's bounding box instead of on the hub, which sits at the centroid and put every label on top of the records it names. - Camera eases into place instead of cutting after a 2.4s delay, keeps re-fitting while the layout spreads, and yields permanently once the reader pans or zooms. Only the always-present legend is reserved in the fit; the transient drawer no longer costs a quarter of the canvas. Vocabulary - Aligned with the effort-graph glossary: Primitives not "node kinds", Relations not "Decision chain", relation groups named after the edges the CLI uses, and the relation descriptions rewritten to match the glossary's claims (`invalidates` says a record was *wrong*, not that it is "no longer valid"). - The header counts primitives and lifecycle rather than nodes and edges: half the edges are synthesised membership spokes, and "173 nodes" says a dot appeared where "3 proposed Decisions" says someone owes a call. - Relation rows render `directionHint` instead of a bare arrow glyph. Craft, a11y, and bug fixes - Node retraction never rendered: the scene resolved metadata from the current query result, so a deleted record vanished instantly while its edges withdrew gracefully. Metadata is now cached until the simulation drops the node. - `computeLineDistances()` ran inside `useFrame`, allocating a fresh `Float32BufferAttribute` per dashed edge per frame. Now recomputed only when the vertex count changes. - Selection committed on pointer-down while left-drag pans, so panning from a record opened the drawer. Now commits on pointer-up within an 8px slop. - Records gained an oversized invisible circular hit mesh sized from `viewport.factor`, guaranteeing a 44 CSS px target at any zoom. - Canvas is keyboard reachable: arrow keys walk a stable order, Enter opens, Escape closes, the camera follows focus, and moves are announced politely. - Type floor raised from 9-10px to 11-12px. Labels no longer change font weight on selection (it reflowed text under the pointer) and dropped their per-label backdrop blur. - Status pill has a fixed width and counts use tabular figures, so live updates cannot shift the header. - Theme follows `prefers-color-scheme` at runtime until the user picks a mode, instead of persisting the resolved system value as an explicit choice. - Reduced motion settles the layout before first paint rather than animating every spawn. - Markdown links only become in-graph navigation when the target resolves to a record; fragments and unresolved relative links stayed anchors instead of becoming silent no-op buttons. - Removed dead `ambientLight` (every material is unlit) and moved the atmospheric gradient off `body`, where an opaque app root hid it entirely. Tests: lifecycle derivation and glyph/palette invariants (31 passing). Change-Id: Ia53f416409d71f8f0866d25d6966d800e2ca97bf
Follow-up from an adversarial review pass on the encoding change. Functional bugs - Pointer hit radius was inverted. R3F hard-codes `viewport.factor` to 1 for orthographic cameras, so the "44 CSS px" calculation produced a constant 22 world units - a 176px target at the default zoom and 22px at the minimum, exactly backwards. It now divides by live `camera.zoom` (one world unit is `zoom` CSS px for an R3F ortho frustum) and caps the padding at half the resting gap between records, so a generous target can never swallow a neighbour and make selection depend on scene order. - The first click permanently disabled camera auto-fit. OrbitControls fires `start` on pointer-down before anything moves, so tapping a record counted as a pan and could strand records off-screen while the layout was still spreading. Takeover is now detected from raw wheel and drag-past-threshold input, which also avoids `change` events emitted by our own easing. - `BlockerRing` rotated unconditionally. It is the only motion left once the layout settles, so under `prefers-reduced-motion` the canvas never came to rest. Now frozen. - The reduced-motion warm-up ran 240 synchronous physics steps on every live generation, not just the first. With O(n^2) repulsion that is a multi-second freeze on a large graph - an accommodation worse than the animation it replaces. Now full only on first sync. - A failed refetch pinned the view to stale data forever: the generation marker advanced before the request. It now advances on success, rolls back on failure so the generation can be retried, and refuses to let an out-of-order response overwrite newer data. Encoding - The query is now assembled from schema introspection. Flatbread derives its schema from records on disk, so a relation field only exists once some record uses it - and the fixed document omitted `supersedes`/`superseded_by` on Findings and Constraints. A Finding has no state field, so supersession is its only retirement signal, and dropping it rendered retired evidence as live. Risk lineage and mitigation were likewise unreachable. - Constraint moved from a teal hexagon to a green bar. Blue Finding and teal Constraint collapse to near-identical cyan under tritanopia, and a hexagon is the least separable non-circle in the set, so both channels were weak at once. A bar also says "boundary" better than a hexagon did. - Levelled record chroma. At equal lightness, chroma was the only intensity variance, so Constraint at 0.11 read as less important than Risk at 0.17 - a ranking the datamodel does not make. - Resolved the hue collisions the Constraint move created: relation strokes keep one accent (h196, in the gap primitives leave between Constraint and Finding) for resolution, and invalidation drops its red, which collided with Risk. The "this was wrong" claim is carried more precisely by ghosting the invalidated record than by tinting the line pointing at it. - Retired records were near-invisible in light mode (~1.8:1 against white). White leaves far less headroom above a record's lightness than black leaves below it, so the light-mode lightness push shrank and opacity rose. - Legend line samples stopped drawing per-weight stroke widths. three.js `LineBasicMaterial` ignores `linewidth`, so the legend was advertising a distinction the canvas cannot render; weight now scales dash length, which is what actually varies. - The legend's blocker sample shows an Issue diamond inside the warning triangle. A triangle inside a triangle read as a Risk. Accessibility - Opening a record moves focus to the drawer heading and returns it to the canvas on close. Without this, Enter appeared to open something a screen reader user could never read - the body and relations stayed unreachable. Physics - Cluster separation buckets member indices (CSR layout) instead of scanning every node per cluster pair, and floors the centroid distance rather than relying on the integrator's step clamp to absorb a near-divide-by-zero. - A realized Risk now counts in the header. It is `settled` on the aliveness axis, so it was falling out of every total - and it is the record a reader most needs surfaced. CI - Wired `examples/effort-viz` into root `test` and `typecheck`. Its 46 tests and its TypeScript were previously never checked by any root script. Also journals the upstream writer defect this encoding works around (iss-writedecision-with-supersedes-leaves-the-superse--by624gyf21ex42sv): the create path for `supersedes` appends the reverse projection but never transitions the target Decision's state, while the `Supersede` retro-link mutation does. Deriving lifecycle from edges is correct regardless, since edges also cover legacy and hand-edited records. Change-Id: Ia2a4f16bc9c4883477b5e7134c1619c6a1a50883
…abels The primitive key is a two-column grid, and the gap between columns was barely larger than the gap between a glyph and its own label. A reviewer reading the panel paired each glyph with the label to its left and reported the whole key off by one. Proximity now groups each pair unambiguously. Change-Id: Id2bd3a2f719cf92437f05846f2a96a3cb8965e00
`LiveSchemaReloader` gained a required `subscribe` member when SSE generation events landed, but two test fakes were not updated. Both files therefore failed to compile, and because ts-node's `TSError` renders as an opaque `[Object: null prototype]` under Node 22, ava reported only "Non-error object" with no message - so `pnpm test` had been failing with no usable diagnostic. Root `typecheck` only covers `@flatbread/proof`, so nothing type-checks `packages/**` ahead of the suite and a compile error in a test file can only surface this way. Change-Id: I086d6a2884e9d0a3f222f678f529e5d80c5753f5
|
Queued — the merge queue status continues in this comment ↓. |
While FitCamera is easing toward a pending fit target, reader pan/zoom must clear that target immediately and stop writing the camera — takeover was only checked after the ease branch returned, so mid-ease input lost until the ease finished. Drop the world-space hit-padding cap (`MAX_HIT_PADDING_WORLD = 3`). At minZoom (~0.5) the 44 CSS px diameter needs ~44 world units of radius; the cap shrunk on-screen hits far below that floor. Hit scale is now `max(glyphRadius, (MIN_HIT_DIAMETER_PX / 2) / zoom)`. Change-Id: Iafbbf69f503a2f03fdb915083b3a9c7131765f70
Add supersedes/superseded_by to Risk RELATION_FIELDS (edges are the only retirement signal). When schema is null, select scalars only so the query does not hard-error against a live Flatbread schema that has not grown every optional relation yet. Cover Risk supersession, null-schema safety, and partial-schema filtering in query.test.ts. Change-Id: Id7f6c0c239530af70dca0a23ef527a117b01b76b
Treat deferred as a ResolveIssue resolution (settled, not open) so deferred blockers no longer get open-blocker treatment. Swap evidence directionHints to match record→Finding edges, drop the stale invalidation warning-hue comment, and align README glyph/retired-state wording with the encoding. Change-Id: Ibb90e54cc22e54f52909144ac47365c1709c1aa6
Keep the last successful schema probe across introspection blips so a transient SCHEMA_PROBE failure cannot fall back to an unsafe relation selection. Defer the generation pill and graph paint until a refetch commits, drop older-than-committed SSE responses before setNodes/setEdges, and roll the request watermark plus pill back on failure. Label SSE loss as Disconnected and query/schema failures as Error. Change-Id: If0309220fcfc9f4bac706215d19963d886724c73
Change-Id: Ia05f2dcfc4b75b660ef2263dc11fd1bfa6d75307
Change-Id: I535829f53973d9cbb2bfd9f8eabf6680e6147ace
Change-Id: I7000fd6518cff4b3b8ab4b26d0d4c60f70ce7cce
Change-Id: Iee592ae50c60892d7e43a3a5dd9d69b0876b3e0d
There was a problem hiding this comment.
Stale comment
Review verdict
REQUEST_CHANGES — consensus HIGHs on false-
liveunder null/sticky schema, missinginvalidated_byparity, DetailDrawer rejection-as-supersession copy, plus documented retirement-signal cases without behavioral tests (normalize danglingsuperseded_by, lifecyclerejected_by).Adversarial DAG (grok-4.5 high + composer-2.5):
correctness-and-contracts,test-coverage-robustness,effort-viz-legibility,dx-and-examples,docs-and-positioning→ judge.Consensus findings
- False-
live/ under-selection (useEffortGraphLiveprobe fallback +setStatus('live'),querynull schema) — retirement edges can be omitted while status still reads healthy.invalidated_byasymmetry — supersession is bidirectional; Finding invalidation is forward-only despite effort-graph reverse projections.- DetailDrawer overturn copy — edge-
rejectedreuses “Replaced by…” while the badge says Rejected.- Missing
rejected_by/ dangling-superseded_bycoverage — documented retirement defenses lack behavioral tests.Coverage plan (priority)
normalize.test.ts— edge: superseder absent; keepsuperseded_bylifecycle.test.ts— negative:rejected_byoverturnsaccepted; edge: already-rejecteddoes not setoverturnedByEdgeuseEffortGraphLive.test.ts— negative: probe-fail must not report unmarkedlive; transport-loss →disconnectedvserrorquery.test.ts— positive: polymorphic fields without braces; ref fields with{ id }lifecycle/normalize— reverseinvalidated_byafter catalog parity- physics/glyphs — simulation cluster/aspect + pointy
glyphExtent(MED/LOW)Reviewer scoreboard
- correctness-and-contracts: 7 findings, 6 coverage gaps, signal:HIGH
- effort-viz-legibility: 6 findings, 5 coverage gaps, signal:HIGH
- dx-and-examples: 5 findings, 5 coverage gaps, signal:HIGH
- docs-and-positioning: 5 findings, 3 coverage gaps, signal:MED
- test-coverage-robustness: 8 findings, 7 coverage gaps, signal:HIGH
Pedantry filtered. Inline comments cover the actionable HIGH blockers; MED/LOW (FitCamera, legend opacity, README counts, crumb Issue body contradiction) are in the plan / follow-ups.
Sent by Cursor Automation: Flatbread PR Review
| title: 'Implement Crumb Trail rename across packages, CLI, paths, and skills' | ||
| kind: gap | ||
| status: open | ||
| status: wontfix |
There was a problem hiding this comment.
HIGH (docs-and-positioning) — Frontmatter is wontfix/resolved_by, but the body still says Crumb Trail branding and “Implementation remains pending,” inviting agents to reopen cancelled rename work.
Minimal fix: Append a short resolution note that the rename is cancelled and Effort Graph is the product name. (Same issue on the Crumb Graph twin Issue.)
When relationship fields cannot be confirmed, keep showing records but label the connection Partial instead of Live so retirement links are not mistaken for complete. Split rejected overturn drawer copy, cover dangling superseded_by and rejected_by, and close out the cancelled Crumb rename Issues. Co-authored-by: Cursor <cursoragent@cursor.com> Change-Id: I063a0ae8113b6b75a6d3d152f51dc97aa21af7cc
|
@Mergifyio queue |
🛑 The pull request has been removed from the queue
|
|
@Mergifyio queue |
Merge Queue Status
This pull request spent 43 seconds in the queue, including 8 seconds running CI. Required conditions to merge
|
fc3634f
into
toeknee/effort-viz-viewport-decision-chain-99c2
There was a problem hiding this comment.
Review verdict
BLOCK — reverse/Decision-sourced invalidation still never reaches the lifecycle index, so overturned records can paint live under a Live/Partial pill (any BLOCKER rule; also consensus HIGH across four reviewers).
Latest sync (4386ad7) fixed several prior findings: edge-rejected drawer copy, Partial status, dangling-superseded_by retention test, and disconnected vs error coverage. The invalidation hole and wontfix Issue body contradiction remain.
Blocking / high (priority)
RELATION_FIELDSis forward-only for invalidation — Findings selectinvalidatesbut neverinvalidated_by; Decisions never selectinvalidateseven though Write* can emit them.buildLifecycleIndexonly indexes forwardinvalidates— noinvalidated_bymirror of supersession dual-indexing.- Normalize /
GraphEdgeKindomitinvalidated_by— reverse edges dropped before lifecycle even if queried. - Wontfix rename Issues still lead with pending Crumb implementation while frontmatter says cancelled.
- Superseded Crumb Decision gained
superseded_bybut still reads as current brand (state: accepted+ body).
Coverage plan (must-ship)
query.test.ts— Finding/Decisioninvalidated_by+ Decisioninvalidateswhen schema lists them (pos/neg).lifecycle.test.ts— reverse-onlyinvalidated_byretires (incl. Decision stillaccepted).normalize.test.ts— danglinginvalidated_by/rejected_byretained (parity withsuperseded_by).
Disputed (not blocking this round)
Sticky last-good schema → Live after probe failure: document in README; do not require a Partial code flip unless product wants it.
Perspectives
correctness-and-contracts, test-coverage-robustness, effort-viz-legibility (HIGH / grok-4.5), dx-and-examples, docs-and-positioning (LOW / composer-2.5) → judge.
Sent by Cursor Automation: Flatbread PR Review
| allFindings: [ | ||
| 'derives_from', | ||
| 'invalidates', | ||
| 'supersedes', | ||
| 'superseded_by', | ||
| 'evidence', | ||
| ], | ||
| allDecisions: [ | ||
| 'derives_from', |
There was a problem hiding this comment.
BLOCKER (test-coverage-robustness; consensus HIGH: correctness-and-contracts, effort-viz-legibility, dx-and-examples) — Invalidation is forward-only here: Findings select invalidates but never invalidated_by, and Decisions never select invalidates even though Write* paths can emit them. A victim whose only on-disk signal is invalidated_by (or whose invalidator is a non-Finding) never enters the edge set, so canvas/drawer stay live.
Minimal fix: Mirror supersession — add invalidated_by to Finding and Decision catalogs; add invalidates to every Write*-capable collection that can carry it (at least allDecisions); keep probe-gating. Do not put invalidated_by under REF_SUBSELECTION (polymorphic id list like invalidates). Also restore invalidated_by?: … on RelationFields (~154–166).
| case 'invalidates': | ||
| invalidated.add(edge.target); | ||
| break; | ||
| case 'rejected_by': | ||
| rejected.add(edge.source); |
There was a problem hiding this comment.
HIGH (correctness-and-contracts, test-coverage-robustness, effort-viz-legibility) — Supersession dual-indexes (superseded_by source + supersedes target), but invalidation only indexes forward invalidates→target. Reverse-only / dangling invalidated_by cannot retire a node even after the query catalog fix.
Minimal fix: case 'invalidated_by': invalidated.add(edge.source) with supersession-style reverse dedupe when both directions exist. Add tests for reverse-only retirement (Decision still declaring accepted).
| /** | ||
| * Drop reverse projections that duplicate an authoritative forward edge. | ||
| * | ||
| * The writer materializes both sides of a supersession: the newer record gets | ||
| * `supersedes` and the older one gets `superseded_by`. Rendering both draws two | ||
| * opposing arrows between the same pair and lists the relation twice in the | ||
| * drawer, which reads as a mutual link when the datamodel has exactly one | ||
| * authoritative direction. | ||
| */ | ||
| function dropRedundantReverseEdges(edges: Map<string, GraphEdge>): void { | ||
| for (const edge of [...edges.values()]) { | ||
| if (edge.kind !== 'superseded_by') continue; | ||
| if (edges.has(edgeId('supersedes', edge.target, edge.source))) { | ||
| edges.delete(edge.id); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * Edges pointing at records the query didn't return are deliberately kept. | ||
| * They render nothing (the renderer bails on a path shorter than two points), | ||
| * but a `superseded_by` whose superseder is missing is still the only signal |
There was a problem hiding this comment.
HIGH (test-coverage-robustness; same hole cross-cutting) — Dangling superseded_by is kept as the sole retirement signal when the superseder is absent, but invalidated_by is not in POLYMORPHIC_RELATIONS / GraphEdgeKind, so reverse invalidation is dropped before lifecycle even if queried.
Minimal fix: Add invalidated_by to GraphEdgeKind + POLYMORPHIC_RELATIONS (and relation-legend meta); retain dangling reverses. Add dangling invalidated_by / rejected_by normalize tests for parity.
|
|
||
| Resolution: cancelled as `wontfix`; product name is Effort Graph; resolved by `dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13`; do not reopen rename work. |
There was a problem hiding this comment.
HIGH (docs-and-positioning) — Frontmatter/resolved_by say wontfix, but the body still leads with Crumb Trail branding and pending implementation, inviting agents to reopen cancelled rename work. Same pattern on the Crumb Graph twin Issue.
Minimal fix: Rewrite the body lead to past-tense cancellation (Effort Graph retained per dec-keep-effort-graph…); demote stale Crumb copy to historical context.
| superseded_by: | ||
| - dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13 |
There was a problem hiding this comment.
HIGH (docs-and-positioning) — This PR adds superseded_by but leaves state: accepted and a body that still commits to Crumb Trail as the product name. Frontmatter, edges, and prose disagree.
Minimal fix: Repair via reindexer (state: superseded) per iss-writedecision-with-supersedes-…, or add a top-of-body supersession banner pointing at dec-keep-effort-graph… until repaired.
| updates the canvas. The status pill shows connecting / live / partial / | ||
| disconnected / error and the current generation. **Partial** means records | ||
| loaded but relationship fields could not be confirmed yet — retirement links | ||
| may be missing until the next successful schema probe. |
There was a problem hiding this comment.
MED (docs-and-positioning; disputed with dx-and-examples) — README defines Partial as “relationship fields could not be confirmed yet,” but sticky last-good after a failed probe still reports Live. Operators can trust Live while newly grown relation fields stay unselected.
Minimal fix (this round): One sentence that sticky cached schema keeps Live; only a successful re-probe picks up new relation fields. Do not require a Partial code flip unless product wants it.


Summary of changes
The Effort Graph viz could not answer its most basic question: what kind of record is this?
nodeColorhashed hue from the effortId and gave node kind only a ±0.06 lightness nudge, so every record in a cluster rendered as the same coloured circle. A correct per-kind palette already existed inRelationLegend.tsxand was dead code — and the legend drew its six swatches from the broken function, so it showed six identical dots next to six different labels. Lifecycle state was never drawn on the canvas at all.Encoding
lib/glyphs.tsso silhouette cannot imply an importance ranking, and record chroma is kept level for the same reason.Lifecycle
lib/lifecycle.tsderives effective state from edges. Forward edges are authoritative and frontmatterstatecan lag them — a Decision replaced through an inlinesupersedeskeepsstate: accepted, so reading the field labelled retired reasoning as committed, on the record a reader most needs to get right. Retired records now fade with a struck-through label, and the drawer explains why its frontmatter disagrees.Badges are keyed on
primitive:state: a Risk's "accepted" means we chose to live with this hazard and must not borrow a Decision's reassuring green. Added the missingwontfix,mitigated,realized, and Effort status vocabulary. Open blocker Issues wear an amber warning outline.Layout, camera, and vocabulary
.agents/skills/effort-graph/glossary.md: Primitives not "node kinds", Relations not "Decision chain", relation groups named after the edges the CLI uses, and descriptions rewritten to match the glossary's claims (invalidatessays a record was wrong, not "no longer valid"). The header counts primitives and lifecycle rather than nodes and edges — half the edges are synthesised membership spokes, and "173 nodes" says a dot appeared where "3 proposed Decisions" says someone owes a call.Bugs fixed along the way
viewport.factorto1for orthographic cameras, so the "44 CSS px" calculation produced a constant 22 world units — a 176px target at default zoom and 22px at minimum, exactly backwards.starton pointer-down before anything moves, so tapping a record counted as a pan.computeLineDistances()allocated a freshFloat32BufferAttributeper dashed edge per frame.BlockerRingignoredprefers-reduced-motionentirely.supersedes/superseded_byon Findings and Constraints — and a Finding has no state field, so supersession is its only retirement signal.ambientLight(every material is unlit) and moved the atmospheric gradient offbody, where an opaque app root hid it entirely.Craft and accessibility
Type floor raised from 9–10px to 11–12px. Labels no longer change font weight on selection (it reflowed text under the pointer) and dropped their per-label backdrop blur. The status pill has a fixed width and counts use tabular figures. The canvas is keyboard reachable — arrow keys walk a stable order, Enter opens, Escape closes, the camera follows focus, moves are announced politely, and focus moves into the drawer and back. This is a focus proxy, not a full DOM mirror of the graph; the README says so. Theme follows
prefers-color-schemeat runtime until the user picks a mode.Also in this PR
wontfix). This is what the retired-record encoding is demonstrating on the canvas.iss-writedecision-with-supersedes-leaves-the-superse--by624gyf21ex42sv):WriteDecisionwith an inlinesupersedesappends the reverse projection but never transitions the target Decision's state, while theSupersederetro-link mutation does. Not fixed here — it also lets a superseded Decision mitigate a Risk, which deserves its own change.pnpm teston this branch.LiveSchemaReloadergained a requiredsubscribewhen SSE events landed, but two test fakes were not updated, so both files failed to compile. Because ts-node'sTSErrorrenders as an opaque[Object: null prototype]under Node 22, ava reported only "Non-error object" with no message. Pre-existing atf1b2ae6, verified by bisecting against the merge-base in clean worktrees.examples/effort-vizinto roottestandtypecheck— its tests and TypeScript were previously never checked by any root script.Closes #
Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
pnpm verifyis green: 323 ava tests plus 46 effort-viz tests (lifecycle derivation, glyph/palette invariants, cluster forces, supersession normalization).Does this introduce any non-backwards compatible changes?
Example-app only, plus two test-fake fixes.
nodeColor/nodeOklchwere removed fromexamples/effort-viz/lib/oklch.ts, which is not a published package.Does this include any user config changes?