physics: imp nodes get the MIDPOINT depth anchor between premises and conclusion

Owner ruling (2026-09-30, live-testing loop — supersedes 02a58db's
exemption): implication nodes ARE depth-anchored after all. Their anchor
is the average of the HIGHEST premise row and the conclusion's row —
depth(imp) = (max(premise depths) + depth(conclusion)) / 2 — so the
junction always sits strictly above every premise and strictly below
the conclusion (a conclusion is >= highest premise + 1 in the logical
graph). Uniform across modes: a half-row in vertical, a ring midway in
radial (owner confirmed both).

Implementation:
- layout.ts: new ImpLink type; SimOptions/incrementalLayout gain
  impLinks (the derived chain linkage). The rank map is now computed
  once alongside the orphan mask: proposition ranks via Tarjan over the
  logical edges, then each supplied link adds its midpoint entry. An
  imp node WITH an anchor is structured (leaves the orphan mask —
  gravity projections treat it like any ranked node); WITHOUT supplied
  links, arg_ ids keep the legacy 02a58db exemption (honest fallback
  for callers that predate the field). The depth force reuses the
  precomputed map — one pass, shared.
- deriveView (core/graph.ts) emits impLinks (one per multi-premise
  live chain — single-premise chains mint no imp node); GraphSnapshot
  carries them; graph-view maps them into CanvasData; settleGraphLayout
  and replay-layout forward them (replay activates a junction's link
  exactly when its chain is delivered).

Tests (+3, 1 rewritten): the 02a58db exemption pin became the MIDPOINT
pin (premises row 0, conclusion row -500, junction pinned to -250);
mixed-depth pin (premises at 0 and 1, conclusion 2, junction at the 1.5
half-row — highest premise, not average-of-premises); legacy fallback
pin (no impLinks = never depth-anchored); deriveView impLinks emission
pin (multi-premise only). The horizontal-pole and ORPHAN RULE pins from
5554c3a survive unchanged — proposition orphans are untouched.
This commit is contained in:
amosblomqvist
2026-09-30 04:52:27 +03:00
parent 9f64019087
commit 35d8bc1a0c
9 changed files with 162 additions and 24 deletions
+4
View File
@@ -127,6 +127,9 @@ export interface CanvasData {
* drawn edges (legacy). Optional so non-graph callers of the
* renderer stay simple. */
logicalEdges?: CanvasEdge[];
/** The imp chain linkage — the depth pass anchors each junction at
* the premises/conclusion midpoint (owner, 2026-09-30). */
impLinks?: ReadonlyArray<{ id: string; premises: ReadonlyArray<string>; conclusion: string }>;
clusters: CanvasCluster[];
colors: CanvasColors;
/** Body font family (labels must match the app typography). */
@@ -1310,6 +1313,7 @@ export class GraphCanvasRenderer {
const t = this.physicsTuning;
this.sim = createGraphSimulation(data.nodes, data.edges, {
logicalEdges: data.logicalEdges,
impLinks: data.impLinks,
gravity: { x: this.simCenter.x, y: this.simCenter.y, strength: t.gravity },
alphaDecay: LIVE_ALPHA_DECAY,
velocityDecay: t.velocityDecay,
+1
View File
@@ -1383,6 +1383,7 @@ export function GraphView() {
})),
edges: snapshot.edges.map((e) => ({ from: e.from, to: e.to })),
logicalEdges: snapshot.logicalEdges.map((e) => ({ from: e.from, to: e.to })),
impLinks: snapshot.impLinks,
clusters: snapshot.clusters.map((c) => ({ id: c.id, label: c.label, members: c.members })),
colors: {
node: tokenHex(NODE_COLOR_TOKEN),