chains-primary: stored chains replace typed edges; edges become a derived view

Owner rulings 2026-09-30: reasoning chains are the stored truth; the graph
canvas renders a lossy derived image. No migration, no backcompat — old
graph data was wiped.

- contracts: GraphChain (arg_-minted id, premises, conclusion) replaces
  typed edges; GraphEdge survives only as the untyped derived arrow;
  GraphSnapshot carries stored chains; supports edges retired entirely
- store: chains.jsonl ledger (edges.jsonl purged at load alongside legacy
  events.jsonl); readChains/appendChain seam; merge dedupes by
  (sorted premises, conclusion), re-assertions append under the same arg_ id
- core: deriveView mints implication nodes (name ⟹, statement via
  formatChainLine) plus premise→imp→conclusion arrows; clusters stay
  proposition-only (birthSessionPartition); chainsForSelection now groups
  STORED chains (chain rides iff every member selected) — independent
  arguments for one conclusion never union; chainsToGraph/chainsToEdges die
- extraction: extractor submits chains natively (expandFindings deleted);
  auditor receives propositions only; supports fan-out gone
- deletion: full death of a proposition kills every referencing chain;
  arg_-prefixed node deletes kill that chain; chainsRemoved stats
- UI: every arrow renders the ⟹ double shaft; supports toggle + thickness
  slider and detail-card edge sections deleted (card keeps name +
  proposition); chain table + practice pipeline read stored chains
- tests: 1188 green across the new seams
This commit is contained in:
amosblomqvist
2026-09-30 03:22:52 +03:00
parent a3aabb3a57
commit 4985b6f9fa
28 changed files with 669 additions and 726 deletions
+17 -17
View File
@@ -1,6 +1,7 @@
import type { GraphEdge, GraphNode, Position } from "./contracts.ts";
import type { GraphChain, GraphNode, Position } from "./contracts.ts";
import { incrementalLayout } from "./layout.ts";
import { birthSessionPartition } from "./session-clusters.ts";
import { deriveView, getGraph } from "./graph.ts";
/**
* The merge-time settle: recompute the resting layout from the CURRENT
@@ -10,6 +11,12 @@ import { birthSessionPartition } from "./session-clusters.ts";
* commit (client positions become the anchor set). Synchronous — it must
* finish before the next /api/graph fetch reflects the change.
*
* CHAINS-PRIMARY (owner, 2026-09-30): the settle lays out the DERIVED
* view — propositions AND the derived implication nodes (one per chain,
* arg_ ids) over the derived untyped arrows — so positions exist for
* every node the canvas renders. Implication nodes participate in the
* physics as regular nodes (rendering specialization comes later).
*
* SPLIT FROM layout.ts deliberately (2026-09-13): layout.ts is in the
* browser barrel, and the settle needs the cluster partition for the
* cluster centroid pull — this module must not ship in the client bundle.
@@ -18,27 +25,20 @@ import { birthSessionPartition } from "./session-clusters.ts";
*/
export function settleGraphLayout(graphStore: {
readNodes(): GraphNode[];
readEdges(): GraphEdge[];
readChains(): GraphChain[];
readPositions(): Record<string, Position>;
writePositions(positions: Record<string, Position>): void;
}): void {
const nodes = graphStore.readNodes();
if (nodes.length === 0) return;
// Node LINES may repeat an id (link-appends) — dedupe, first defines.
const seen = new Set<string>();
const unique = nodes.filter((n) => {
if (seen.has(n.id)) return false;
seen.add(n.id);
return true;
});
const edges = graphStore.readEdges();
const graph = getGraph(graphStore);
if (graph.nodes.length === 0) return;
const view = deriveView(graph);
const previous = new Map(Object.entries(graphStore.readPositions()));
// The cluster partition for the centroid pull: the SAME birth-session
// partition the snapshot renders (derived from provenance on read —
// core/session-clusters.ts, wave 7) — one cluster concept everywhere.
// The unique list is the deduped set the partition needs (lines may
// repeat an id — first defines).
const communities = birthSessionPartition(unique);
const next = incrementalLayout(unique, edges, previous, { clusters: communities });
// PROPOSITIONS ONLY (owner, 2026-09-30): implication nodes never
// partition; the stored world clusters, its image follows.
const communities = birthSessionPartition(graph.nodes);
const next = incrementalLayout(view.nodes, view.edges, previous, { clusters: communities });
graphStore.writePositions(Object.fromEntries(next));
}
}