diff --git a/docs/architecture.md b/docs/architecture.md
index fe4d6ae..e0c3fad 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -332,8 +332,8 @@ flowchart TB
IMPORT["POST /api/graph/import (notes front door)
deterministic src id = sha256(path) — idempotent re-imports"] -->|"importNotes: shared pipeline, second extractor prompt"| RUN
RUN --> BATCH["batchRuns — greedy token-budgeted batches of oldest uncovered TEACHER-PROSE messages"]
BATCH --> EXT["EXTRACTOR (LLM) — sees ONLY the session markdown view
(tree-blind, graph-blind; shared engine: core/session-markdown.ts)"]
- EXT --> AUD["AUDITOR (LLM, agentic loop) — sees findings ONLY, never the source;
explores the graph via find / list_sessions / list_propositions tools,
judges identity (link-or-create, equivalence bias) + may draw bridging edges"]
- AUD --> MERGE["MERGER (deterministic, core/graph.ts) — the ONLY ledger writer:
identity via validated linkTo · bridging-edge resolution · dedupe edges · coverage checkpoint"]
+ EXT --> AUD["AUDITOR (LLM, agentic loop) — sees findings ONLY, never the source;
explores the graph via find / list_sessions / list_propositions tools,
judges identity ONLY (link-or-create, equivalence bias) — never mints edges"]
+ AUD --> MERGE["MERGER (deterministic, core/graph.ts) — the ONLY ledger writer:
identity via validated linkTo · dedupe edges · coverage checkpoint"]
MERGE --> SETTLE["settleGraphLayout — resting positions ledger"]
MERGE -->|"failure at ANY stage"| NOTHING["checkpoints nothing — the next trigger re-runs"]
```
@@ -342,7 +342,7 @@ flowchart TB
**Serialized audit, parallel extracts — per-chunk jobs** (owner ruling 2026-09-28): every chunk (notes file) and every batch (chat coverage) is its own JOB — registry row, usage record, failure domain. Extracts fan out in parallel across all jobs and sources, throttled by a process-wide 3-permit extract semaphore on `globalThis` (the provider-facing rate stance; a job waiting for a permit shows `queued`). The audit→merge section remains a process-wide FIFO critical section — a mutex on `globalThis` (the same module-instance idiom as the run registries): before any auditor LLM call or graph read, a job waits (`waiting` phase) until every earlier job's merge has landed, so each auditor explores the graph as it actually stands — two concurrent audits would each link into a graph the other is about to change, the structural mint of duplicate nodes. Failure isolation: one job failing (bad chunk, degraded model output) is absorbed at the fan-out — its messages stay uncovered and honestly re-runnable while its siblings still merge; the run returns what actually merged. Abort is source-wide: every job of a source's fan-out dies together, and merged jobs keep their checkpoints. The status feed shows one row per job (`file.md · 2/5` suffixes for chunked sources), and each job persists its own usage record (failed jobs bill their partial spend too).
-**Division of trust** (the pipeline's core rule): the extractor is fully trusted and is the only stage that ever sees the source. The auditor never sees the transcript — it receives the findings JSON alone and explores the existing graph through read-only tools (`find`, `list_sessions`, `list_propositions` — statement text included, no timestamps) to reach its verdicts. VERDICTS-ONLY (owner ruling 2026-09-28): the auditor's submit is `{ links, edges }` — identity links (which findings propositions ARE existing nodes) plus bridging edges (`follows`/`supports` connections between existing graph nodes and the batch's propositions). It never restates the findings; the merger joins findings + verdicts, so an order of magnitude less is emitted and there is no structural way for the auditor to alter or drop extractor output. Equivalence bias (owner ruling 2026-09-27): paraphrases, rewordings, and compressions of the same claim MERGE; only genuinely distinct propositions stay apart. The deterministic merger validates and appends: link resolution, dedupe by `(from, to, type)`, coverage checkpoints. There is no drop channel — nothing rejects the extractor's output.
+**Division of trust** (the pipeline's core rule): the extractor is fully trusted and is the only stage that ever sees the source. The auditor never sees the transcript — it receives the findings JSON alone and explores the existing graph through read-only tools (`find`, `list_sessions`, `list_propositions` — statement text included, no timestamps) to reach its verdicts. VERDICTS-ONLY (owner ruling 2026-09-28): the auditor's submit is `{ links }` — identity links only (which findings propositions ARE existing nodes). The edges argument is GONE (owner, same evening): the auditor never mints edges — cross-source connection happens solely through identified equivalences, and relations between propositions come from the extractor alone. It never restates the findings; the merger joins findings + verdicts, so an order of magnitude less is emitted and there is no structural way for the auditor to alter or drop extractor output. Equivalence bias (owner ruling 2026-09-27): paraphrases, rewordings, and compressions of the same claim MERGE; only genuinely distinct propositions stay apart. The deterministic merger validates and appends: link resolution, dedupe by `(from, to, type)`, coverage checkpoints. There is no drop channel — nothing rejects the extractor's output.
**Coverage, not quiz-counting** (supersedes the v1 every-N-quiz-answers trigger): each message's *view* (the compact line format `core/coverage.ts` has always hashed) is hashed into a per-chat fingerprint ledger (`coverage.jsonl`); "covered" means exactly "a run committed on this content." The prompt view is the richer markdown document — a deliberate 2026-09-27 split: re-formatting presentation never re-opens covered history. Uncovered = fingerprint diff, never dual-tracked; runs are greedy token-budgeted batches (`graph.extractionChunkTokens` — a settings knob, default 4096, floor 500, read fresh each turn) of the oldest uncovered messages — quiz-less sessions extract too. A coarse `sessions.jsonl` checkpoint still marks each merged run. The trigger seam (`turn-service.ts`) is trigger-agnostic from the pipeline's side: when a better mechanism lands (session end, idle, explicit flush), only the seam changes. Single-flight per source; the run registry powers the coverage UI and true abort.
diff --git a/src/agent/subagents/extraction.ts b/src/agent/subagents/extraction.ts
index aa35101..b90e86b 100644
--- a/src/agent/subagents/extraction.ts
+++ b/src/agent/subagents/extraction.ts
@@ -45,9 +45,10 @@ import { auditorGraphTools } from "../tools/graph.ts";
* proposition, linkTo an existing graph node or create —
* decided by EXPLORING the graph through tools (find /
* list_sessions / list_propositions), not by an injected
- * graph dump (context bloat, owner 2026-09-27). It may
- * also draw BRIDGING edges from existing graph nodes to
- * the batch's propositions.
+ * graph dump (context bloat, owner 2026-09-27). Links are
+ * its ONLY deliverable — it never mints edges (owner
+ * 2026-09-28: the graph connects across sources solely
+ * through identified equivalences).
* merger (core) mergeFindings() → deterministic ledger appends
*
* The events ledger is GONE (2026-09-27): quiz outcomes and understanding
@@ -159,9 +160,10 @@ const findingsProps = {
const findingsSchema = Type.Object(findingsProps);
// VERDICTS-ONLY (owner, 2026-09-28): the auditor never alters the
-// extractor's findings, so it does not restate them — links + bridging
-// edges are its entire deliverable. An order-of-magnitude smaller submit
-// than the old full-findings echo.
+// extractor's findings, so it does not restate them — links are its
+// ENTIRE deliverable (owner, 2026-09-28 evening: the edges argument is
+// GONE; the auditor never mints edges. The graph connects across sources
+// solely through identified equivalences — a link, never an inference).
const auditSchema = Type.Object({
links: Type.Array(
Type.Object({
@@ -173,10 +175,6 @@ const auditSchema = Type.Object({
}),
{ description: "Identity verdicts — which findings propositions ARE existing graph nodes. A proposition absent from here = create a new node. Pass [] when none — never omit the field." },
),
- edges: Type.Array(edgeSchema, {
- description:
- "BRIDGING edges ONLY — new edges welding this batch into the existing graph, with an EXISTING graph node id as one endpoint (or both) and/or a findings proposition id. NEVER restate the extractor's own edges. Pass [] when none — never omit the field.",
- }),
});
// The notes front door's shapes are IDENTICAL to the session shapes now
@@ -213,8 +211,9 @@ function healStringifiedArrays(raw: unknown): unknown {
// ── the submit seam (owner, 2026-09-28) ──
//
-// Both top-level fields on both submit tools are REQUIRED — a payload
-// missing `edges`/`links` fails validation instead of silently merging
+// Every top-level field on each submit tool is REQUIRED — a payload
+// missing `edges` (findings) or `links` (review) fails validation instead
+// of silently merging
// edge-less (the degraded-retry hole: the model's first truncated
// attempt failed validation, then the retry passed with propositions
// only — optional edges ??-filled [] — merging a pile of nodes with no
@@ -230,7 +229,6 @@ function fillFindings(raw: Static): SessionFindings {
function fillAudit(raw: Static): AuditVerdicts {
return {
links: raw.links,
- edges: raw.edges,
};
}
@@ -464,18 +462,12 @@ IDENTITY — links, per proposition:
propositions — different claims about the world — stay apart. You are NOT guarding against wrong merges; you are
hunting for right ones.
-BRIDGING EDGES — the one place you create edges:
-- You may add edges whose endpoints are an EXISTING graph node id and one of the batch's propositions (use the
- existing node's real id as "from" or "to"). This is how the new material gets welded into the old graph: when the
- existing graph's content makes a proposition follow, or grounds it, draw the edge.
-- Keep edges typed like the extractor's: "follows" = derivable; "supports" = grounds without entailing.
-- Never invent propositions. NEVER restate the extractor's own edges — they merge as-is.
+Your deliverable is ONLY the identity verdicts: "links". NEVER restate the propositions or the extractor's edges —
+the merger already has them. You NEVER create edges — relations between the new propositions come from the
+extractor alone, and the batch connects to the existing graph ONLY through the equivalences you identify. Your
+entire deliverable is ONE submit_review call.
-Your deliverable is ONLY the verdicts: "links" + "edges" (bridging ONLY). NEVER restate the propositions or the
-extractor's edges — the merger already has them. Your entire deliverable is ONE submit_review call.
-
-Include EVERY top-level field of the call even when empty — "links": [] when nothing links, "edges": [] when there
-are no bridges. NEVER omit a field because it is empty.`;
+Include the "links" field even when empty — "links": [] when nothing links. NEVER omit it because it is empty.`;
/** The auditor — one shape for both front doors now (the notes schema was
* identical once attribution died). An agentic loop: graph-exploration
@@ -490,7 +482,7 @@ function runAuditor(
return runSubmitAgent({
role: "auditor",
toolName: "submit_review",
- toolDescription: "Submit the verdicts ONLY: identity links + bridging edges — never the propositions themselves.",
+ toolDescription: "Submit the identity verdicts ONLY (links) — never the propositions themselves.",
parameters: auditSchema,
fill: fillAudit,
systemPrompt: AUDITOR_INSTRUCTIONS,
@@ -834,7 +826,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
);
console.info(
`[extraction ${jobKey}] audit: ${findings.propositions.length} propositions through, ` +
- `${auditResult.links.length} linked to existing nodes, ${auditResult.edges.length} bridging edges`,
+ `${auditResult.links.length} linked to existing nodes`,
);
console.info(`[extraction ${jobKey}] auditor done in ${Math.round(performance.now() - t0)}ms`);
return auditResult;
diff --git a/src/core/contracts/graph.ts b/src/core/contracts/graph.ts
index 4ff6a04..62ee671 100644
--- a/src/core/contracts/graph.ts
+++ b/src/core/contracts/graph.ts
@@ -36,9 +36,7 @@ export interface EmissionStats {
/** One provenance point — message-granular (DECIDED 2026-09-02): nodes and
* edges record every (chat, message) where they came up, deep-linkable to
- * the exact transcript point. Auditor-drawn bridging edges carry the chat
- * but NO message anchor (nothing in the transcript asserts them — the
- * auditor is the asserter); messageId is "" there. */
+ * the exact transcript point. */
export interface ProvenancePoint {
/** The chat this point anchors to — or a notes source id ("src_…"):
* imports carry no message anchors (provenance is the file itself,
@@ -211,14 +209,12 @@ export interface ExtractedProposition {
}
/** One proposed relation. Endpoints are extractor-local proposition ids
- * ("p1", …); the AUDITOR may also emit bridging edges whose from/to is an
- * EXISTING graph node id (connecting the new content to what the graph
- * already holds) — the merger resolves both. */
+ * ("p1", …) — the extractor is the ONLY stage that ever proposes edges
+ * (the auditor's edges argument is gone, owner 2026-09-28 evening). */
export interface ExtractedEdge {
- /** Proposition id (or, for auditor-drawn bridges, a graph node id)
- * that is followed / supported. */
+ /** Proposition id that is followed / supported. */
from: string;
- /** Proposition id (or graph node id) that follows / is supported. */
+ /** Proposition id that follows / is supported. */
to: string;
type: EdgeType;
}
@@ -244,14 +240,13 @@ export interface IdentityLink {
/** The auditor's output — VERDICTS ONLY (owner, 2026-09-28): the auditor
* never alters the extractor's findings, so it does not restate them;
* the deterministic merger joins findings + verdicts. `links` = the
- * identity verdicts (local id ⇄ existing node); `edges` = BRIDGING
- * edges the auditor draws from existing graph nodes to the batch's
- * propositions (endpoints: existing node ids and/or local proposition
- * ids). No drop channel — the auditor never rejects; the extractor is
- * fully trusted (2026-09-27). */
+ * identity verdicts (local id ⇄ existing node) — the auditor's ENTIRE
+ * deliverable (owner, 2026-09-28 evening: the edges argument is GONE —
+ * the auditor never mints edges; the graph connects across sources
+ * solely through identified equivalences). No drop channel — the
+ * auditor never rejects; the extractor is fully trusted (2026-09-27). */
export interface AuditVerdicts {
links: IdentityLink[];
- edges: ExtractedEdge[];
}
/** The GET /api/graph payload — everything the home view's PURE renderer needs. */
diff --git a/src/core/graph.ts b/src/core/graph.ts
index ee484aa..76e3350 100644
--- a/src/core/graph.ts
+++ b/src/core/graph.ts
@@ -11,10 +11,11 @@
* from the source ALONE (whole session as markdown,
* or raw notes). Fully trusted: no stage re-checks it.
* auditor (LLM) → AuditVerdicts: identity links (local id → an existing
- * node, or create) via graph-exploration tools, plus
- * any BRIDGING edges it draws between existing graph
- * nodes and the new propositions. Never sees the
- * source; never drops.
+ * node, or create) via graph-exploration tools —
+ * links ONLY (owner, 2026-09-28 evening: the auditor
+ * never mints edges; cross-source connection happens
+ * solely through identified equivalences). Never sees
+ * the source; never drops.
* merger (HERE) → mergeFindings(): deterministic ledger appends
*
* The merger is deliberately the ONLY stage that touches the ledgers, and it
@@ -199,8 +200,9 @@ export function mergeFindings(
if (line.provenance.some((pt) => pt.chatId === chatId)) assertedByChat.add(key);
}
const emitEdge = (edge: ExtractedEdge): void => {
- const resolve = (endpoint: string): string | undefined =>
- mapping.get(endpoint) ?? (existingIds.has(endpoint) ? endpoint : undefined);
+ // Extractor-local ids only — the auditor's bridging edges are gone
+ // (owner, 2026-09-28 evening), so raw node-id endpoints no longer occur.
+ const resolve = (endpoint: string): string | undefined => mapping.get(endpoint);
const from = resolve(edge.from);
const to = resolve(edge.to);
if (!from || !to || from === to) return; // unresolvable endpoint — skip
@@ -210,8 +212,7 @@ export function mergeFindings(
assertedByChat.add(key);
stats.edges++;
};
- for (const edge of findings.edges) emitEdge(edge);
- for (const edge of verdicts.edges) emitEdge(edge);
+ for (const edge of findings.edges) emitEdge(edge); // the extractor's edges — the ONLY edge source
// 3. Checkpoint — THE COVERAGE DISCIPLINE (vision-extraction-coverage.md):
// only now, after every append succeeded, is the batch covered. The
diff --git a/tests/extraction.test.ts b/tests/extraction.test.ts
index 3c036fa..3dcd58a 100644
--- a/tests/extraction.test.ts
+++ b/tests/extraction.test.ts
@@ -367,7 +367,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
it("stringified-payload heal (owner, 2026-09-28): propositions arriving as ONE JSON string still validate and merge", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
- const verdict: AuditVerdicts = { links: [], edges: [] };
+ const verdict: AuditVerdicts = { links: [] };
// The weak-model failure from the OpenRouter log: the whole array
// emitted as a JSON-STRING ("propositions.0: must be object").
@@ -400,7 +400,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
});
const verdict: AuditVerdicts = {
links: [{ id: FINDINGS.propositions[0]!.id, linkTo: existingId }],
- edges: [],
};
const pipeline = createExtractionPipeline({
@@ -515,7 +514,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
// Empty verdicts: everything creates (verdicts-only, 2026-09-28).
- const verdict: AuditVerdicts = { links: [], edges: [] };
+ const verdict: AuditVerdicts = { links: [] };
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
@@ -534,7 +533,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
it("engineFor fills the stages (model slots wave): the chain replaces config pins", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
- const verdict: AuditVerdicts = { links: [], edges: [] };
+ const verdict: AuditVerdicts = { links: [] };
const consulted: string[] = [];
const pipeline = createExtractionPipeline({
chatStore,
@@ -561,7 +560,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
it("direct test values still WIN over engineFor (the priority pin)", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
- const verdict: AuditVerdicts = { links: [], edges: [] };
+ const verdict: AuditVerdicts = { links: [] };
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
@@ -597,7 +596,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: silentModel(),
- auditor: submitModel("submit_review", { links: [], edges: [] }),
+ auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await failingExtractor.runExtraction(chatId), { nodes: 0, edges: 0 });
assert.equal(ledgers.readCheckpoints().length, 0);
@@ -619,7 +618,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
engineFor: (role) =>
- role === "extractor" ? fauxEngine([countingStep]) : submitModel("submit_review", { links: [], edges: [] }),
+ role === "extractor" ? fauxEngine([countingStep]) : submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await mixed.runExtraction(chunkyId), { nodes: 2, edges: 1 }, "batch 1's merge survives batch 2's failure");
assert.equal(invocations, 2);
@@ -647,7 +646,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const phaseOf = () => extractionStatuses().find((r) => r.chatId === chatId)?.phase;
const verdict: AuditVerdicts = {
links: [{ id: FINDINGS.propositions[0]!.id, linkTo: existingId }],
- edges: [],
};
const pipeline = createExtractionPipeline({
chatStore,
@@ -681,7 +679,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", findingsA),
- auditor: gatedSubmitModel("submit_review", { links: [], edges: [] }, gate, () => {
+ auditor: gatedSubmitModel("submit_review", { links: [] }, gate, () => {
aAuditing = true;
}),
});
@@ -698,7 +696,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
// THE pin: when B's auditor is finally invoked, A's merge must
// already be in the ledgers — B explores the post-merge graph,
// which is the entire point of the gate.
- auditor: submitModel("submit_review", { links: [], edges: [] }, () => {
+ auditor: submitModel("submit_review", { links: [] }, () => {
bAuditorCalled = true;
bSawAlphasMerge = ledgers.readNodes().some((n) => n.name === "Alpha claim");
}),
@@ -735,7 +733,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", single("pa", "Alpha claim", "Alpha statement")),
- auditor: gatedSubmitModel("submit_review", { links: [], edges: [] }, gate),
+ auditor: gatedSubmitModel("submit_review", { links: [] }, gate),
});
const runA = pipelineA.runExtraction(a.chatId);
await until(() => extractionStatuses().some((r) => r.chatId === a.chatId && r.phase === "auditing"));
@@ -745,7 +743,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", single("pb", "Beta claim", "Beta statement")),
- auditor: submitModel("submit_review", { links: [], edges: [] }, () => {
+ auditor: submitModel("submit_review", { links: [] }, () => {
bAuditorCalled = true;
}),
});
@@ -767,7 +765,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", single("pc", "Gamma claim", "Gamma statement")),
- auditor: submitModel("submit_review", { links: [], edges: [] }),
+ auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipelineC.runExtraction(c.chatId), { nodes: 1, edges: 0 });
});
@@ -787,7 +785,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const usageSeen: (number | undefined)[] = [];
const verdict: AuditVerdicts = {
links: [{ id: FINDINGS.propositions[0]!.id, linkTo: existingId }],
- edges: [],
};
const pipeline = createExtractionPipeline({
chatStore,
@@ -833,7 +830,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: hangingModel(), // hangs until aborted — no step finishes
- auditor: submitModel("submit_review", { links: [], edges: [] }),
+ auditor: submitModel("submit_review", { links: [] }),
});
const runPromise = pipeline.runExtraction(chatId);
@@ -1057,7 +1054,7 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
engineFor: (role) =>
role === "extractor"
? extractorStep()
- : submitModel("submit_review", { links: [], edges: [] }),
+ : submitModel("submit_review", { links: [] }),
getChunkTokens: () => 20, // 80 chars/line-groups → several chunks
});
const stats = await pipeline.importNotes({ filePath: "/vault/big.md", text });
@@ -1092,7 +1089,7 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
- engineFor: (role) => (role === "extractor" ? noEdges() : submitModel("submit_review", { links: [], edges: [] })),
+ engineFor: (role) => (role === "extractor" ? noEdges() : submitModel("submit_review", { links: [] })),
});
// Isolated job failure (per-chunk jobs, owner 2026-09-28): the run
// resolves zeros — no throw — and still nothing merged.
@@ -1398,7 +1395,7 @@ describe("per-chunk parallel jobs (owner, 2026-09-28)", () => {
chatStore,
graphStore: ledgers,
engineFor: (role) =>
- role === "extractor" ? fauxEngine([abortingStep]) : submitModel("submit_review", { links: [], edges: [] }),
+ role === "extractor" ? fauxEngine([abortingStep]) : submitModel("submit_review", { links: [] }),
getChunkTokens: () => 20,
});
const runPromise = pipeline.importNotes({ filePath: "/vault/parallel.md", text: wideText() });
@@ -1440,7 +1437,7 @@ describe("per-chunk parallel jobs (owner, 2026-09-28)", () => {
{ stopReason: "toolUse" },
);
const reviewMessage = fauxAssistantMessage(
- [fauxToolCall("submit_review", { links: [], edges: [] } as unknown as ToolCall["arguments"], { id: "review-call" })],
+ [fauxToolCall("submit_review", { links: [] } as unknown as ToolCall["arguments"], { id: "review-call" })],
{ stopReason: "toolUse" },
);
const slowExtract: FauxResponseStep = async () => {
@@ -1485,7 +1482,7 @@ describe("the session-deletion guard (chat deleted mid-run)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
- auditor: submitModel("submit_review", { links: [], edges: [] }, () => chatStore.deleteChat(chatId)),
+ auditor: submitModel("submit_review", { links: [] }, () => chatStore.deleteChat(chatId)),
});
const stats = await pipeline.runExtraction(chatId);
diff --git a/tests/graph.test.ts b/tests/graph.test.ts
index 00649f5..181263e 100644
--- a/tests/graph.test.ts
+++ b/tests/graph.test.ts
@@ -256,7 +256,7 @@ function fixtureFindings(): SessionFindings {
/** The auditor's verdicts when nothing links: an empty verdict set is a
* valid full review (verdicts-only, owner 2026-09-28). */
function fixtureVerdicts(): AuditVerdicts {
- return { links: [], edges: [] };
+ return { links: [] };
}
/** The chat every merger test folds (shared, so ordering is intentional). */
@@ -359,7 +359,7 @@ describe("the merger (mergeFindings)", () => {
},
// VERDICTS-ONLY: the identity call is a link entry, not a field
// on the proposition (owner, 2026-09-28).
- { links: [{ id: "q1", linkTo: alphaId }], edges: [] },
+ { links: [{ id: "q1", linkTo: alphaId }] },
graphDeps,
sessionSource(chat2.id),
);
@@ -397,7 +397,7 @@ describe("the merger (mergeFindings)", () => {
],
edges: [],
},
- { links: [], edges: [] },
+ { links: [] },
graphDeps,
{ foldedAt: "2026-09-13T00:00:00.000Z" },
);
@@ -430,7 +430,7 @@ describe("the merger (mergeFindings)", () => {
{ from: "p1", to: "p3", type: "follows", },
],
},
- { links: [], edges: [] },
+ { links: [] },
graphDeps,
{ foldedAt: "2026-09-13T00:00:00.000Z" },
);
@@ -482,7 +482,7 @@ describe("the merger (mergeFindings)", () => {
edges: [],
},
// empty verdicts — identity is the auditor's call alone
- { links: [], edges: [] },
+ { links: [] },
graphDeps,
sessionSource(chat3.id),
);
@@ -506,7 +506,7 @@ describe("the merger (mergeFindings)", () => {
],
edges: [],
},
- { links: [], edges: [] },
+ { links: [] },
graphDeps,
{ foldedAt: "2026-09-13T00:00:00.000Z" },
);
@@ -610,7 +610,7 @@ describe("session deletion (deleteChatTraces + deleteChatSession, wave 6)", () =
],
edges: [{ from: "p1", to: "p2", type: "follows", }],
};
- mergeFindings(dA.id, findingsA, { links: [], edges: [] }, graphDeps, sessionSource(dA.id));
+ mergeFindings(dA.id, findingsA, { links: [] }, graphDeps, sessionSource(dA.id));
// Resolve through THIS session's mappings — the shared ledger holds
// lookalike statements from earlier tests, so statement-lookup would
// link dB to the wrong node.
@@ -636,7 +636,7 @@ describe("session deletion (deleteChatTraces + deleteChatSession, wave 6)", () =
],
edges: [{ from: "q1", to: "q2", type: "follows", }],
};
- mergeFindings(dB.id, findingsB, { links: [{ id: "q1", linkTo: xId }], edges: [] }, graphDeps, sessionSource(dB.id));
+ mergeFindings(dB.id, findingsB, { links: [{ id: "q1", linkTo: xId }] }, graphDeps, sessionSource(dB.id));
const zId = graphStore.readMappings().find((m) => m.chatId === dB.id && m.sourceId === "q2")!.graphNodeId;
// Coverage rows (both chats) + positions garnish — the deletion