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
+14
View File
@@ -457,6 +457,20 @@ describe("the merger (mergeFindings)", () => {
assert.ok(mine, "the trio is one cluster");
assert.equal(mine!.id, "src_clusterfixture");
assert.equal(mine!.label, "/vault/cluster notes.md", "a notes file is a birth-session — the label is its path");
// Chains-primary split (owner, 2026-09-30): `nodes` is the canvas
// image (propositions + derived implication nodes), `propositions`
// is the stored truth alone — the chain table's input. The store is
// shared across this suite, so assert the INVARIANT, not counts:
// image = truth + one imp node per fully-live stored chain, and the
// truth never carries an arg_ id.
const stored = graphStore.readChains();
const live = new Set(snap.propositions.map((n) => n.id));
const liveChains = stored.filter((c) => live.has(c.conclusion) && c.premises.every((p) => live.has(p)));
assert.ok(snap.propositions.every((n) => !n.id.startsWith("arg_")), "the stored-truth list never carries imp nodes");
assert.equal(snap.nodes.length, snap.propositions.length + liveChains.length, "image = truth + one imp node per live chain");
const imp = snap.nodes.filter((n) => n.id.startsWith("arg_"));
assert.ok(imp.length === liveChains.length && imp.every((n) => snap.propositions.every((p) => p.id !== n.id)), "the extra nodes are exactly the derived implication nodes");
});
it("derives the sessions index in graphSnapshot", () => {