graph: persist the arg_ implication nodes — the splatter fix
Diagnosed (owner report 2026-10-06, 'splattered from the middle on every navigation'): the canvas's world is 350 nodes — 262 stored propositions + 88 derived arg_ implication nodes the chain arrows bend through — but the drag-commit sanitizer accepted only stored proposition ids, and writePositions REPLACES the file: every drag-commit purged the arg_ entries, so every mount re-seeded them on a ~1306-unit ring (seedMissingNodePositions) — arrows streaking out of the middle, bbox inflated, fit zoomed out. A drag annealed them back (impLinks springs), the commit stripped them again. Fix: the sanitizer's known set = deriveView(getGraph()).nodes — propositions AND arg_ ids (stable: minted from the chain ledger). The grew-check becomes the honest test — some CURRENT view node absent from the payload (a merge landed mid-drag) → exactly one absorbing settle; full coverage → none (the old proposition-count comparison broke once args counted). Live ledger backfilled once: 88 arg_ positions seeded at their chains' centroids, propositions byte-identical (no settle, no re-anneal). Pinned in tests/commit-positions.test.ts (hermetic LH_DATA_DIR): args persist, unknown ids drop, no-settle on full coverage, absorbing settle on a stale payload, garbage sanitizing.
This commit is contained in:
@@ -9,9 +9,17 @@
|
||||
*
|
||||
* Every body is sanitized: garbage coordinates clamp or drop, unknown
|
||||
* ids are discarded, nothing throws.
|
||||
*
|
||||
* KNOWN IDS = THE CANVAS'S WORLD (owner fix, 2026-10-06 — the splatter
|
||||
* bug): the derived view's nodes — stored propositions AND the derived
|
||||
* arg_ implication nodes. The canvas persists all of them; stripping
|
||||
* the args here purged their ledger entries on every drag-commit, so
|
||||
* each mount re-seeded them on a giant ring (the "splattered from the
|
||||
* middle" report) until a drag annealed them back. arg_ ids are stable
|
||||
* (minted from the chain ledger), so persisting them is sound.
|
||||
*/
|
||||
import type { Position } from "../core/contracts.ts";
|
||||
import { getGraph } from "../core/graph.ts";
|
||||
import { deriveView, getGraph } from "../core/graph.ts";
|
||||
import { settleGraphLayout } from "../core/layout-settle.ts";
|
||||
import { stores } from "./stores.ts";
|
||||
|
||||
@@ -27,11 +35,11 @@ export function commitPositions(input: unknown): Record<string, Position> {
|
||||
if (typeof input !== "object" || input === null) return {};
|
||||
const raw = input as Record<string, unknown>;
|
||||
|
||||
// Only ids the graph actually has (first line wins, like getGraph).
|
||||
const known = new Map<string, boolean>();
|
||||
for (const line of stores.graphStore.readNodes()) {
|
||||
if (!known.has(line.id)) known.set(line.id, true);
|
||||
}
|
||||
// Only ids the canvas's world actually has: stored propositions plus
|
||||
// the DERIVED arg_ implication nodes (deriveView of the current graph
|
||||
// — the same derivation the snapshot serves, so ids agree by
|
||||
// construction).
|
||||
const known = new Set(deriveView(getGraph(stores.graphStore)).nodes.map((n) => n.id));
|
||||
|
||||
const positions: Record<string, Position> = {};
|
||||
for (const [id, value] of Object.entries(raw)) {
|
||||
@@ -50,8 +58,15 @@ export function commitPositions(input: unknown): Record<string, Position> {
|
||||
stores.graphStore.writePositions(positions);
|
||||
|
||||
// The graph grew past what the client knew (a merge landed mid-drag):
|
||||
// absorb the new nodes with the client's rest state as anchors.
|
||||
const unique = getGraph(stores.graphStore).nodes.length;
|
||||
if (unique > Object.keys(positions).length) settleGraphLayout(stores.graphStore);
|
||||
// absorb the new nodes with the client's rest state as anchors. The
|
||||
// honest test: some node of the CURRENT view is absent from the
|
||||
// client's payload — proposition or arg_ alike (the old count
|
||||
// comparison broke once args counted as persistable).
|
||||
for (const id of known) {
|
||||
if (!(id in positions)) {
|
||||
settleGraphLayout(stores.graphStore);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
Reference in New Issue
Block a user