fix: chain table reads the stored propositions, not the derived canvas image

The owner's live test surfaced it: after importing notes, the chains view
listed implication nodes as bogus singleton "chains" — and each carried
the same arg_ id as the real stored chain, tripping React's duplicate-key
error (chain-table.tsx:372).

Root cause: graph-view passed snapshot.nodes — the derived canvas view
(propositions + implication nodes) — into ChainTableOverlay, whose
chainsForSelection turns every unchained node into a singleton chain.
An implication node is never a chain member (members are proposition
ids), so every imp node minted a bogus singleton with a colliding id;
its row was even selectable, leaking arg_ ids into practice input.

Fix (owner-approved shape): GraphSnapshot gains `propositions` — the
stored-truth node list riding beside `chains` for exactly this class of
consumer. The chain table now receives snapshot.propositions; the canvas
keeps snapshot.nodes (imp nodes are meant to be drawn). Practice pipeline
unaffected (it already read getGraph, which is propositions-only).

Test: the snapshot split pinned as an invariant in graph.test.ts — image
= truth + one imp node per fully-live stored chain; truth never carries
an arg_ id.
This commit is contained in:
amosblomqvist
2026-09-30 03:41:41 +03:00
parent a2cb1447f7
commit 8d72de596a
4 changed files with 20 additions and 1 deletions
+4
View File
@@ -307,6 +307,10 @@ export interface GraphSnapshot {
/** Proposition nodes + the DERIVED implication nodes (one per chain,
* id = chain id) — what the canvas renders. */
nodes: GraphNode[];
/** The STORED proposition nodes alone (no derived implication nodes)
* — the stored truth, like `chains`: consumers that read the world
* (the chain table) take this list, never the canvas image. */
propositions: GraphNode[];
/** DERIVED untyped arrows: premise → imp → conclusion. A lossy image
* of the stored chains — rebuilt after every change, never stored. */
edges: GraphEdge[];
+1
View File
@@ -409,6 +409,7 @@ export function graphSnapshot(deps: GraphDeps): GraphSnapshot {
return {
nodes,
propositions: graph.nodes,
edges: view.edges,
chains: graph.chains,
sessions,