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
+8 -24
View File
@@ -65,7 +65,6 @@ import {
sanitizeRegionTuning,
type PhysicsTuning,
type RegionTuning,
type EdgeType,
INCREMENTAL_GRAVITY,
LINK_DISTANCE,
type DepthAnchorMode,
@@ -86,14 +85,13 @@ export interface CanvasNode {
label: string;
}
/** One directed edge (follows | supports); arrow points prerequisite → dependent. */
/** One directed edge of the DERIVED view (chains-primary, 2026-09-30):
* premise → implication node → conclusion, UNTYPED — every arrow is an
* implication, so every arrow renders the double-shaft glyph (⟹) +
* arrowhead (DECIDED 2026-09-14, docs/vision-edge-glyph.md). */
export interface CanvasEdge {
from: string;
to: string;
/** Relation type: `follows` renders the double-shaft glyph (⟹),
* `supports` the plain single line (DECIDED 2026-09-14,
* docs/vision-edge-glyph.md). */
type: EdgeType;
}
/** One cluster region: member node ids + the LLM-coined label (or null). */
@@ -623,10 +621,6 @@ export class GraphCanvasRenderer {
private builtVersion = -1; // version the cache was built at
private nodesPath: Path2D | null = null;
private shaftsPath: Path2D = new Path2D();
/** Supports shafts draw THINNER (owner, 2026-09-29): their own path +
* stroke pass at edgeWidth × supportsWidth. Arrowheads share the one
* heads path at full geometry (owner pick: shaft only). */
private supportsShaftsPath: Path2D = new Path2D();
private headsPath: Path2D = new Path2D();
private regionCanvas: HTMLCanvasElement | null = null;
/** World rect the bitmap covers (drawImage target coords). */
@@ -1237,32 +1231,26 @@ export class GraphCanvasRenderer {
// relation types draw the same arrow for now — the double-shaft
// glyph was rejected (2026-09-15); differentiation is pending a new
// approach. Two owner rulings 2026-09-29: the supports draw toggle
// (off skips the paths entirely — a draw-only hide; the sim still
// feels them) and the supports SHAFT thickness split (supports get
// their own path stroked at edgeWidth × supportsWidth; heads stay
// full-geometry in the one shared path).
// CHAINS-PRIMARY (2026-09-30): one path family — every derived arrow
// is an implication, double-shafted + arrowheaded uniformly.
const arrowLen = settings.arrowLength;
const shafts = new Path2D();
const supportsShafts = new Path2D();
const heads = new Path2D();
for (const e of data.edges) {
if (!settings.showSupports && e.type === "supports") continue;
if (!isVisible(e.from) || !isVisible(e.to)) continue;
const a = this.nodeById.get(e.from);
const b = this.nodeById.get(e.to);
if (!a || !b) continue;
const g = arrowGeometry(a, b, nodeR, nodeR, arrowLen);
if (!g) continue;
const path = e.type === "supports" ? supportsShafts : shafts;
path.moveTo(g.shaftFrom.x, g.shaftFrom.y);
path.lineTo(g.shaftTo.x, g.shaftTo.y);
shafts.moveTo(g.shaftFrom.x, g.shaftFrom.y);
shafts.lineTo(g.shaftTo.x, g.shaftTo.y);
heads.moveTo(g.tip.x, g.tip.y);
heads.lineTo(g.left.x, g.left.y);
heads.lineTo(g.right.x, g.right.y);
heads.closePath();
}
this.shaftsPath = shafts;
this.supportsShaftsPath = supportsShafts;
this.headsPath = heads;
// Cluster regions are NOT built here: they rasterize into their own
@@ -1639,10 +1627,6 @@ export class GraphCanvasRenderer {
ctx.lineWidth = settings.edgeWidth / k;
ctx.lineCap = "round";
ctx.stroke(this.shaftsPath);
if (settings.showSupports) {
ctx.lineWidth = Math.max(settings.edgeWidth * settings.supportsWidth, 0.5) / k;
ctx.stroke(this.supportsShaftsPath);
}
ctx.fill(this.headsPath);
}