The persisted (chatId, sourceId)→node table was a shared namespace across independent chunk/batch jobs: each chunk's extractor mints local ids fresh (p1..pN), so later chunks collided with earlier ones — propositions silently skipped as 'already resolved' (coverage checkpointed anyway: permanent silent loss) and edges resolved onto the wrong nodes (the 'everything links to everything' mush). Owner ruling 2026-09-29: mergeFindings resolves THIS job's ids in memory, nothing persists between jobs; cross-job identity is the auditor's linkTo verdicts alone. - contracts: NodeMapping dies; NotesSource.contentFp — sha256 of the text at the last run, the re-import guard (unchanged re-import = TRUE no-op, no model calls, no usage; changed text re-runs; legacy unstamped sources re-run once) - merger: job-local resolution map, in-submit duplicate ids skip once, edges resolve extractor-local ids only - ports/store: appendMapping/readMappings, LEDGER_FILES.mappings, rewriteLedgers.mappings gone (stale data/graph/mappings.jsonl just stops being read) - session-clusters: birthSessionOf is provenance-only — the legacy mapping-line fallback for provenance-empty notes nodes dies - deletion: no mapping rows to strip; stats lose mappingsRemoved - replay-layout batches nodes by first provenance point - graph.test pins the new independence semantics (re-merge re-creates; duplicate ids within one submit resolve once); extraction tests pin per-chunk merges + the fingerprint guard
44 lines
2.0 KiB
TypeScript
44 lines
2.0 KiB
TypeScript
import type { GraphEdge, GraphNode, Position } from "./contracts.ts";
|
|
import { incrementalLayout } from "./layout.ts";
|
|
import { birthSessionPartition } from "./session-clusters.ts";
|
|
|
|
/**
|
|
* The merge-time settle: recompute the resting layout from the CURRENT
|
|
* graph + the positions ledger, and replace the ledger. Called by the
|
|
* extraction pipeline after every merge (the sole writer) and by the
|
|
* positions-commit endpoint when the graph grew under a live-physics
|
|
* commit (client positions become the anchor set). Synchronous — it must
|
|
* finish before the next /api/graph fetch reflects the change.
|
|
*
|
|
* 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.
|
|
* Server code imports it DIRECTLY; it is never re-exported through
|
|
* src/core/index.ts.
|
|
*/
|
|
export function settleGraphLayout(graphStore: {
|
|
readNodes(): GraphNode[];
|
|
readEdges(): GraphEdge[];
|
|
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 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 });
|
|
graphStore.writePositions(Object.fromEntries(next));
|
|
} |