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
+12 -2
View File
@@ -316,7 +316,12 @@ export function getGraph(graphStore: Pick<GraphStore, "readNodes" | "readChains"
* regular citizens (half link distance — layout.ts). Dangling chains
* (a member node missing) drop here — the deletion cascades should
* prevent them; this is the honest belt. */
export function deriveView(graph: DerivedGraph): { nodes: GraphNode[]; edges: GraphEdge[]; logicalEdges: GraphEdge[] } {
export function deriveView(graph: DerivedGraph): {
nodes: GraphNode[];
edges: GraphEdge[];
logicalEdges: GraphEdge[];
impLinks: Array<{ id: string; premises: string[]; conclusion: string }>;
} {
const names: Record<string, { name: string }> = {};
for (const node of graph.nodes) names[node.id] = { name: node.name };
const live = new Set(graph.nodes.map((node) => node.id));
@@ -326,6 +331,7 @@ export function deriveView(graph: DerivedGraph): { nodes: GraphNode[]; edges: Gr
// direct premise→conclusion arrows over PROPOSITION ids only —
// what inference semantics (the depth pass) consumes. The drawn edges
// are the canvas image; these are the meaning.
const impLinks: Array<{ id: string; premises: string[]; conclusion: string }> = [];
const logicalEdges: GraphEdge[] = [];
const logicalSeen = new Set<string>();
const emitLogical = (from: string, to: string): void => {
@@ -351,10 +357,13 @@ export function deriveView(graph: DerivedGraph): { nodes: GraphNode[]; edges: Gr
createdAt: chain.createdAt,
provenance: [...chain.provenance],
});
// The junction's linkage — what the depth pass needs for the
// midpoint anchor (owner, 2026-09-30).
impLinks.push({ id: chain.id, premises: [...chain.premises], conclusion: chain.conclusion });
for (const premise of chain.premises) edges.push({ from: premise, to: chain.id });
edges.push({ from: chain.id, to: chain.conclusion });
}
return { nodes: [...graph.nodes, ...impNodes], edges, logicalEdges };
return { nodes: [...graph.nodes, ...impNodes], edges, logicalEdges, impLinks };
}
// ─── the /api/graph payload ──────────────────────────────────────────────────
@@ -442,6 +451,7 @@ export function graphSnapshot(deps: GraphDeps): GraphSnapshot {
propositions: graph.nodes,
edges: view.edges,
logicalEdges: view.logicalEdges,
impLinks: view.impLinks,
chains: graph.chains,
sessions,
fileSources,