delete the mappings ledger — per-job in-memory resolution + notes fingerprint guard

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
This commit is contained in:
amosblomqvist
2026-09-29 02:25:32 +03:00
parent 92146248d2
commit 8355fbabdb
17 changed files with 182 additions and 304 deletions
+7 -44
View File
@@ -38,14 +38,12 @@ function fakeStores(
over: {
chats?: Array<Pick<ChatSummary, "id" | "title" | "createdAt" | "updatedAt">>;
sources?: Array<Pick<NotesSource, "id" | "filePath" | "importedAt">>;
mappings?: Array<{ chatId: string; sourceId: string; graphNodeId: string }>;
} = {},
): GraphDeps {
const edges: GraphEdge[] = [];
const graphStore: GraphStore = {
readNodes: () => nodes,
readEdges: () => edges,
readMappings: () => over.mappings ?? [],
readCheckpoints: () => [],
readCoverage: () => [],
readSources: () =>
@@ -103,46 +101,15 @@ describe("birthSessionPartition", () => {
});
});
// ─── the notes mapping fallback (owner-approved 2026-09-23) ─────────────────
describe("the notes mapping-line fallback", () => {
// The PRODUCTION notes shape: importNotes passes messages: null, so
// nodes are born with EMPTY provenance — the file's anchor is the
// mapping line (interlude live-smoke finding: the provenance-only rule
// left every imported file unclustered and invisible to the prober).
it("empty-provenance nodes birth under their first mapping line's session", () => {
const nodes = [
node("a", "Alpha", []),
node("b", "Beta", []),
node("c", "Gamma", []),
node("d", "Delta", [pt("chat1")]), // provenance wins — mapping ignored
];
const mappings = [
{ chatId: "src_notes", sourceId: "p1", graphNodeId: "a" },
{ chatId: "src_notes", sourceId: "p2", graphNodeId: "b" },
{ chatId: "src_notes", sourceId: "p3", graphNodeId: "c" },
// A chat mapping for a node that ALSO has provenance: never consulted.
{ chatId: "chat2", sourceId: "p4", graphNodeId: "d" },
// A mapping for a node that does not exist: ignored.
{ chatId: "src_notes", sourceId: "p5", graphNodeId: "ghost" },
];
const clusters = birthSessionPartition(nodes, mappings);
assert.deepEqual(clusters, [
{ id: "src_notes", members: ["a", "b", "c"] },
{ id: "chat1", members: ["d"] },
], "size-desc, then id asc — src_notes first");
// No mapping and no provenance: unclustered.
assert.deepEqual(birthSessionPartition([node("x", "X", [])], mappings), []);
});
// ─── notes files as birth sessions (production shape) ────────────────────────
describe("notes files as birth sessions", () => {
// The mapping-line fallback is GONE (owner, 2026-09-29): the mappings
// ledger itself died with per-job in-memory resolution, and every merge
// writes provenance points — the src id IS the birth session.
it("graphSessionIndex lists imported files as kind:file with birth counts (production shape)", () => {
const nodes = [node("a", "Alpha", []), node("b", "Beta", []), node("c", "Gamma", [])];
const nodes = [node("a", "Alpha", [pt("src_abc")]), node("b", "Beta", [pt("src_abc")]), node("c", "Gamma", [pt("src_abc")])];
const deps = fakeStores(nodes, {
mappings: [
{ chatId: "src_abc", sourceId: "p1", graphNodeId: "a" },
{ chatId: "src_abc", sourceId: "p2", graphNodeId: "b" },
{ chatId: "src_abc", sourceId: "p3", graphNodeId: "c" },
],
sources: [{ id: "src_abc", filePath: "/vault/learn vim.md", importedAt: "2026-09-22T00:00:00.000Z" }],
});
const index = graphSessionIndex(deps);
@@ -159,11 +126,7 @@ describe("the notes mapping-line fallback", () => {
});
it("graphSessionPropositions lists the file's births under the src id (a pure birth list, 2026-09-27)", () => {
const deps = fakeStores([node("a", "Alpha", []), node("b", "Beta", [])], {
mappings: [
{ chatId: "src_abc", sourceId: "p1", graphNodeId: "a" },
{ chatId: "src_abc", sourceId: "p2", graphNodeId: "b" },
],
const deps = fakeStores([node("a", "Alpha", [pt("src_abc")]), node("b", "Beta", [pt("src_abc")])], {
sources: [{ id: "src_abc", filePath: "/vault/learn vim.md", importedAt: "2026-09-22T00:00:00.000Z" }],
});
assert.deepEqual(graphSessionPropositions(deps, "src_abc"), [