digest-aware serialized extraction + chain simplification replace the spine stage

Owner live-test verdict on the spine (2026-09-30): no perceptible gain —
the real problems (restatement nodes, shallow re-derivations, same
conclusion from many angles) are mint-time issues a post-hoc chain
minter cannot fix. This arc moves assembly INTO the extractor and adds
the deterministic dedupe the owner specified:

- Digest-aware extraction: each stretch's extractor prompt carries THE
  GRAPH SO FAR (its own source's merged propositions + chains, ids
  verbatim); build on existing ids instead of restating, lean on
  established conclusions as premises. Mixed-namespace chain endpoints
  (fresh p-ids OR existing n_-ids) validated at the submit seam.
- Serialized per source: a source's batches/chunks run strictly in
  order (each extract reads every earlier merge); cross-source runs
  stay parallel; source-wide abort stops the loop. The spine stage,
  role, phase, usage stage, cost row, and setup roster are gone.
- Core-ideas prompt: goal reframed as uncovering core ideas and showing
  how everything derives from the foundations; direction upward, prefer
  lean derivations, never re-derive what the graph already holds.
- Chain simplification (merger maintenance): simplifyChains drops
  provably redundant chains at every merge — prefer-fewest-premises +
  ancestry-cover ([A,B]⟹X with [X,C]⟹Y covers [A,B,C]⟹Y, transitive);
  same-key provenance lines drop together; rewriteLedgers under the
  audit lock; droppedChains counted separately in stats.

Chunk budget stays 8192 (owner: keep for now, maybe lower later).
This commit is contained in:
amosblomqvist
2026-09-30 17:44:37 +03:00
parent 52c02533d1
commit 91341a93da
18 changed files with 734 additions and 879 deletions
+286 -313
View File
@@ -46,21 +46,14 @@ import {
createExtractionPipeline,
extractionStatuses,
transcriptView,
sourceDigest,
EXTRACTION_CORE,
SESSION_PREAMBLE,
NOTES_PREAMBLE,
EXTRACTOR_INSTRUCTIONS,
NOTES_EXTRACTOR_INSTRUCTIONS,
} from "../src/agent/subagents/extraction.ts";
import {
SPINE_CORE,
SPINE_INSTRUCTIONS,
NOTES_SPINE_INSTRUCTIONS,
validateSpine,
buildSpinePrompt,
spineHasDigest,
type SpineSubmit,
} from "../src/agent/subagents/spine.ts";
import { simplifyChains } from "../src/core/graph.ts";
import { ModelsNotConfiguredError, ProviderKeyMissingError, providerKeyMissing, type ModelSlotName } from "../src/core/settings.ts";
import { coverageOf, messageViewLines } from "../src/core/coverage.ts";
import { chatStore } from "../src/store/chats.ts";
@@ -284,15 +277,6 @@ function submitModel(name: string, input: unknown, onCall?: () => void): PiEngin
* the timing control for the serialized-audit tests (owner 2026-09-28):
* the model call has STARTED (onCall fired, the run holds the audit
* gate) but will not resolve until the test opens the gate. */
/** The spine fixture every pipeline construction carries (stage gate is
* strict, owner 2026-09-30): the honest common case — the stretch stands
* alone, empty submit. One-shot; multi-batch tests that need a REAL
* spanning spine build their own submitModel("submit_spine", …). */
function spineModel(): PiEngine {
return submitModel("submit_spine", { propositions: [], chains: [] });
}
function gatedSubmitModel(name: string, input: unknown, gate: Promise<void>, onCall?: () => void): PiEngine {
return fauxEngine([
async () => {
@@ -402,7 +386,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chains: FINDINGS.chains,
});
const pipeline = createExtractionPipeline({ chatStore, graphStore: ledgers, extractor, spine: spineModel(), auditor: submitModel("submit_review", verdict) });
const pipeline = createExtractionPipeline({ chatStore, graphStore: ledgers, extractor, auditor: submitModel("submit_review", verdict) });
const stats = await pipeline.runExtraction(chatId);
assert.deepEqual(stats, { nodes: 2, chains: 1 });
});
@@ -418,7 +402,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor,
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipeline.runExtraction(chatId), { nodes: 2, chains: 1 });
@@ -447,7 +430,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", verdict),
});
const stats = await pipeline.runExtraction(chatId);
@@ -496,7 +478,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const pipeline = createExtractionPipeline({ chatStore, graphStore: ledgers });
await assert.rejects(pipeline.runExtraction(chatId), (err: unknown) => {
assert.ok(err instanceof ModelsNotConfiguredError);
assert.deepEqual(err.envelope.missing, ["cheap", "strong"]); // extractor + auditor cheap, spine strong
assert.deepEqual(err.envelope.missing, ["cheap"]); // extractor + auditor, both cheap-tier
assert.equal(err.envelope.code, "models_not_configured");
return true;
});
@@ -514,7 +496,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor,
spine: spineModel(),
auditor,
providerKeyMissing: (provider, tier) => {
seen.push([provider, tier]);
@@ -542,15 +523,14 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: fauxEngine([]),
spine: spineModel(),
auditor: fauxEngine([]),
providerKeyMissing: (provider, tier) => (calls++ < 2 ? null : providerKeyMissing(provider, [tier])),
providerKeyMissing: (provider, tier) => (calls++ < 1 ? null : providerKeyMissing(provider, [tier])),
});
await assert.rejects(pipeline.runExtraction(chatId), (err: unknown) => {
assert.ok(err instanceof ProviderKeyMissingError, "the auditor's refusal surfaces after extractor + spine passed");
assert.ok(err instanceof ProviderKeyMissingError, "the auditor's refusal surfaces after the extractor passed");
return true;
});
assert.equal(calls, 3, "all three stages were checked, in order");
assert.equal(calls, 2, "both stages were checked, in order");
assert.equal(ledgers.readCoverage().length, 0);
});
@@ -563,7 +543,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", verdict),
});
const stats = await pipeline.runExtraction(chatId);
@@ -589,7 +568,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
engineFor: (role) => {
consulted.push(role);
if (role === "extractor") return submitModel("submit_findings", FINDINGS);
if (role === "spine") return submitModel("submit_spine", { propositions: [], chains: [] });
if (role === "auditor") return submitModel("submit_review", verdict);
return submitModel("submit_labels", { labels: [] });
},
@@ -597,9 +575,9 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const stats = await pipeline.runExtraction(chatId);
assert.equal(stats.nodes, 2, "the chain-fed pipeline ran end to end — both propositions merged");
// The stages were resolved through the chain, in pipeline order
// (extractor → spine → auditor; the labeler is fire-and-forget
// (extractor → auditor; the labeler is fire-and-forget
// garnish for any other role).
assert.deepEqual(consulted.slice(0, 3), ["extractor", "spine", "auditor"]);
assert.deepEqual(consulted.slice(0, 2), ["extractor", "auditor"]);
});
it("direct test values still WIN over engineFor (the priority pin)", async () => {
@@ -610,7 +588,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", verdict),
// If the direct values did NOT win, this throw would surface.
engineFor: (role) => {
@@ -629,7 +606,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
spine: spineModel(),
auditor: silentModel(), // never submits the review
});
// The old run-level throw is gone: one failed job is absorbed at
@@ -643,7 +619,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: silentModel(),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await failingExtractor.runExtraction(chatId), { nodes: 0, chains: 0 });
@@ -666,13 +641,11 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
// 4096 budget: the fixture is sized to split in two (the factory
// default doubled to 8192 with the spine stage, 2026-09-30).
// default doubled to 8192, 2026-09-30).
getChunkTokens: () => 4096,
engineFor: (role) =>
role === "extractor"
? fauxEngine([countingStep])
: role === "spine"
? submitModel("submit_spine", { propositions: [], chains: [] })
: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await mixed.runExtraction(chunkyId), { nodes: 2, chains: 1 }, "batch 1's merge survives batch 2's failure");
@@ -708,7 +681,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
// Each stage's first generate is the observation point: the
// registry must already carry this chat at the right phase.
extractor: submitModel("submit_findings", FINDINGS, () => seen.push(phaseOf())),
spine: spineModel(),
auditor: submitModel("submit_review", verdict, () => seen.push(phaseOf())),
});
await pipeline.runExtraction(chatId);
@@ -735,7 +707,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", findingsA),
spine: spineModel(),
auditor: gatedSubmitModel("submit_review", { links: [] }, gate, () => {
aAuditing = true;
}),
@@ -753,7 +724,6 @@ 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.
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }, () => {
bAuditorCalled = true;
bSawAlphasMerge = ledgers.readNodes().some((n) => n.name === "Alpha claim");
@@ -791,7 +761,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", single("pa", "Alpha claim", "Alpha statement")),
spine: spineModel(),
auditor: gatedSubmitModel("submit_review", { links: [] }, gate),
});
const runA = pipelineA.runExtraction(a.chatId);
@@ -802,7 +771,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", single("pb", "Beta claim", "Beta statement")),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }, () => {
bAuditorCalled = true;
}),
@@ -825,7 +793,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", single("pc", "Gamma claim", "Gamma statement")),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipelineC.runExtraction(c.chatId), { nodes: 1, chains: 0 });
@@ -853,7 +820,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
// During the auditor's generate, the registry must already carry
// the extractor's fully accumulated live usage (2 steps × 10/5).
extractor: submitModel("submit_findings", FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", verdict, () =>
usageSeen.push(extractionStatuses().find((r) => r.chatId === chatId)?.usage.extractor?.steps.at(-1)?.inputTokens),
),
@@ -892,7 +858,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: hangingModel(), // hangs until aborted — no step finishes
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
const runPromise = pipeline.runExtraction(chatId);
@@ -943,7 +908,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: gatedExtractor,
spine: spineModel(),
auditor: silentModel(),
});
@@ -980,7 +944,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: gatedExtractor,
spine: spineModel(),
auditor: silentModel(),
});
// A SECOND instance over DIFFERENT ledgers — the observer a separate
@@ -990,7 +953,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: memoryLedgers(),
extractor: silentModel(),
spine: spineModel(),
auditor: silentModel(),
});
@@ -1017,7 +979,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: silentModel(),
spine: spineModel(),
auditor: silentModel(),
});
await assert.rejects(pipeline.runExtraction("chat_does_not_exist"), /Chat not found/);
@@ -1087,7 +1048,6 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
chatStore,
graphStore: ledgers,
extractor,
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipeline.runExtraction(chatId), { nodes: 3, chains: 1 });
@@ -1123,7 +1083,6 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
chatStore,
graphStore: ledgers,
extractor,
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
// Independent arguments for the same conclusion are separate chains;
@@ -1144,7 +1103,6 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
chatStore,
graphStore: ledgers,
extractor,
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipeline.runExtraction(chatId), { nodes: 2, chains: 1 });
@@ -1189,7 +1147,6 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
chatStore,
graphStore: ledgers,
extractor: retryEngine(bad, FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
// The first submit was schema-VALID — only the chain law caught it;
@@ -1208,7 +1165,6 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
chatStore,
graphStore: ledgers,
extractor: retryEngine(bad, FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipeline.runExtraction(chatId), { nodes: 2, chains: 1 });
@@ -1225,7 +1181,6 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
chatStore,
graphStore: ledgers,
extractor: retryEngine(bad, FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }),
});
assert.deepEqual(await pipeline.runExtraction(chatId), { nodes: 2, chains: 1 });
@@ -1235,12 +1190,16 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
// The core carries the goal + submission mechanics.
assert.match(EXTRACTION_CORE, /JOINTLY entail a conclusion/);
assert.match(EXTRACTION_CORE, /for the same conclusion are separate chains/);
assert.match(EXTRACTION_CORE, /Entailment is the only link that earns a chain/);
assert.match(EXTRACTION_CORE, /Entailment is the only link that earns a\s+chain/);
assert.match(EXTRACTION_CORE, /what a reference on it would teach/);
assert.match(EXTRACTION_CORE, /stand alone for a reader who never saw this source/);
assert.match(EXTRACTION_CORE, /may not lean on the source's own scaffolding/);
assert.match(EXTRACTION_CORE, /stand\s+alone for a reader who never saw this source/);
assert.match(EXTRACTION_CORE, /may not lean on the source's own\s+scaffolding/);
assert.match(EXTRACTION_CORE, /SUBMIT with one submit_findings call/);
assert.match(EXTRACTION_CORE, /"chains": \[\] when the source makes no arguments/);
assert.match(EXTRACTION_CORE, /"chains": \[\] when the source makes no\s+arguments/);
// The digest doctrine (2026-09-30 evening) + the core-ideas language.
assert.match(EXTRACTION_CORE, /THE GOAL: uncover the source's CORE IDEAS/);
assert.match(EXTRACTION_CORE, /everything derives from the foundations/);
assert.match(EXTRACTION_CORE, /build\s+on it\./);
// Assembly: each front door = its preamble + the shared core.
assert.ok(EXTRACTOR_INSTRUCTIONS.startsWith(SESSION_PREAMBLE));
@@ -1250,8 +1209,8 @@ describe("the extractor redesign (owner, 2026-09-30): chains are the deliverable
// The preambles differ in exactly their source description.
assert.notEqual(SESSION_PREAMBLE, NOTES_PREAMBLE);
assert.match(SESSION_PREAMBLE, /a teaching session, rendered as the teacher's prose in reading order/);
assert.match(NOTES_PREAMBLE, /one chunk of the author's personal notes, rendered in reading order/);
assert.match(SESSION_PREAMBLE, /a teaching\s+session, rendered as the teacher's prose in reading order/);
assert.match(NOTES_PREAMBLE, /the\s+author's personal notes, rendered in reading order/);
// NO RULE ARMOR (owner, 2026-09-30): the defensive lists are CUT
// from both contextualizations.
@@ -1274,7 +1233,6 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
graphStore: ledgers,
extractor: submitModel("submit_findings", NOTES_FINDINGS),
// During the auditor's call the registry must show the notes run.
spine: spineModel(),
auditor: submitModel("submit_review", NOTES_VERDICT, () => {
const run = extractionStatuses().find((r) => r.chatId.startsWith("src_"));
seen.push({ kind: run?.kind, label: run?.label });
@@ -1334,8 +1292,6 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
engineFor: (role) =>
role === "extractor"
? extractorStep()
: role === "spine"
? submitModel("submit_spine", { propositions: [], chains: [] })
: submitModel("submit_review", { links: [] }),
getChunkTokens: () => 20, // 80 chars/line-groups → several chunks
});
@@ -1389,7 +1345,6 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", NOTES_FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", NOTES_VERDICT),
});
const first = await mk().importNotes({ filePath: "/vault/learn vim.md", text: NOTES_TEXT });
@@ -1420,7 +1375,6 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
chatStore,
graphStore: ledgers,
extractor: hangingModel(),
spine: spineModel(),
auditor: submitModel("submit_review", NOTES_VERDICT),
});
const runPromise = pipeline.importNotes({ filePath: "/vault/learn vim.md", text: NOTES_TEXT });
@@ -1502,10 +1456,9 @@ describe("the chunked pipeline (extraction coverage)", () => {
chatStore,
graphStore: ledgers,
// 4096 budget: the fixture is sized to split in two (the factory
// default doubled to 8192 with the spine stage, 2026-09-30).
// default doubled to 8192, 2026-09-30).
getChunkTokens: () => 4096,
extractor: fauxEngine([findingsStep((v) => views.push(v)), findingsStep((v) => views.push(v))]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep(), passThroughAuditStep()]),
});
const stats = await pipeline.runExtraction(chatId);
@@ -1554,7 +1507,7 @@ describe("the chunked pipeline (extraction coverage)", () => {
chatStore,
graphStore: ledgers,
// 4096 budget: the fixture is sized to split in two (the factory
// default doubled to 8192 with the spine stage, 2026-09-30).
// default doubled to 8192, 2026-09-30).
getChunkTokens: () => 4096,
// Batch 1 submits and merges; batch 2's extractor call HANGS
// until the abort fires — the dock's abort button, mid-run.
@@ -1570,7 +1523,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
sig?.addEventListener("abort", () => resolve(fauxAssistantMessage("late")), { once: true });
}),
]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep()]),
});
const runPromise = pipeline.runExtraction(chatId);
@@ -1601,7 +1553,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
chatStore,
graphStore: ledgers,
extractor: fauxEngine([findingsStep((v) => reRunViews.push(v))]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep()]),
});
await finisher.runExtraction(chatId);
@@ -1626,7 +1577,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
chatStore,
graphStore: ledgers,
extractor: fauxEngine([findingsStep()]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep()]),
});
await pipeline.runExtraction(chatId);
@@ -1662,7 +1612,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
chatStore,
graphStore: ledgers,
extractor: fauxEngine([findingsStep((v) => reRunViews.push(v))]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep()]),
});
await reOpener.runExtraction(chatId);
@@ -1685,11 +1634,11 @@ describe("per-chunk parallel jobs (owner, 2026-09-28)", () => {
const wideText = () =>
Array.from({ length: 8 }, (_, i) => `note ${i}: ${"x".repeat(90)}`).join("\n");
it("fans out one job per chunk: 3 extract concurrently (the global semaphore), the rest queue, every job is its own feed row", async () => {
it("one job per chunk, SERIALIZED per source: the registry shows one live row at a time, and a source-wide abort ends the run", async () => {
const ledgers = memoryLedgers();
// The extractor call hangs until its ABORT — the observation window:
// with all jobs parked, the registry must show exactly 3 extracting
// and 5 queued, one row per chunk with its part.
// the parked chunk-1 job must be the ONLY live row (later chunks
// mint only when their turn comes, digest order, owner 2026-09-30).
const abortingStep: FauxResponseStep = (_context, options) =>
new Promise((_resolve, reject) => {
const sig = options?.signal;
@@ -1711,26 +1660,21 @@ describe("per-chunk parallel jobs (owner, 2026-09-28)", () => {
const runPromise = pipeline.importNotes({ filePath: "/vault/parallel.md", text: wideText() });
const srcId = extractionStatuses().find((r) => r.chatId.startsWith("src_"))!.chatId;
await until(() => extractionStatuses().filter((r) => r.chatId === srcId).length === 8, 500);
await until(() => extractionStatuses().some((r) => r.chatId === srcId && r.phase === "extracting"), 500);
const rows = extractionStatuses().filter((r) => r.chatId === srcId);
assert.equal(rows.length, 8, "one feed row per chunk job");
assert.equal(rows.filter((r) => r.phase === "extracting").length, 3, "the semaphore admits exactly 3 extractor calls");
assert.equal(rows.filter((r) => r.phase === "queued").length, 5, "the rest wait for an extract slot, not the model");
const parts = rows.map((r) => r.part).filter((p): p is { i: number; n: number } => Boolean(p));
assert.equal(parts.length, 8, "multi-chunk sources stamp every row with its part");
assert.deepEqual(
[...parts].sort((a, b) => a.i - b.i).map((p) => `${p.i}/${p.n}`),
Array.from({ length: 8 }, (_, i) => `${i + 1}/8`),
);
assert.equal(rows.length, 1, "one live row — the source's chunks run strictly in order");
assert.equal(rows[0]!.phase, "extracting");
assert.deepEqual(rows[0]!.part, { i: 1, n: 8 }, "the row carries its part");
// SOURCE-WIDE abort: every chunk job of the fan-out dies together.
// SOURCE-WIDE abort: the parked job dies and the run ENDS — no
// later chunk ever mints (fresh rows would hang un-aborted).
assert.equal(abortExtraction(srcId), true);
assert.deepEqual(await runPromise, { nodes: 0, chains: 0 });
assert.equal(extractionStatuses().filter((r) => r.chatId === srcId).length, 0, "all rows leave the registry");
assert.equal(ledgers.readNodes().length, 0, "nothing merged");
// Per-job usage: all 8 chunk jobs persisted their (failed) spend.
// Per-job usage: the ONE minted job persisted its (failed) spend.
const source = ledgers.readSources().find((src) => src.id === srcId)!;
assert.equal(source.extractions?.length, 8, "one usage record per chunk job — even failed jobs bill honestly");
assert.equal(source.extractions?.length, 1, "only the aborted job bills — later chunks never ran");
assert.ok(source.extractions!.every((entry) => entry.stats === undefined), "no job merged — no stats");
});
@@ -1764,28 +1708,25 @@ describe("per-chunk parallel jobs (owner, 2026-09-28)", () => {
auditInFlight--;
return reviewMessage;
};
const spineMessage = fauxAssistantMessage(
[fauxToolCall("submit_spine", { propositions: [], chains: [] } as unknown as ToolCall["arguments"], { id: "spine-call" })],
{ stopReason: "toolUse" },
);
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
engineFor: (role) =>
role === "extractor"
? fauxEngine([slowExtract])
: role === "spine"
? fauxEngine([() => spineMessage])
: fauxEngine([slowAudit]),
role === "extractor" ? fauxEngine([slowExtract]) : fauxEngine([slowAudit]),
getChunkTokens: () => 20,
});
const stats = await pipeline.importNotes({ filePath: "/vault/parallel-2.md", text: wideText() });
assert.equal(extractPeak, 3, "8 chunks, 3 permits — the peak pins both the fan-out and the cap");
// SERIALIZED PER SOURCE (owner, 2026-09-30 evening): one source's
// chunks run strictly in order — each extractor continues the
// digest of every earlier merge, so the peak within one source is
// ONE concurrent extract. (Cross-source runs still parallelize;
// the 3-permit semaphore still caps those.)
assert.equal(extractPeak, 1, "one source, one extract at a time — strict order");
assert.equal(auditPeak, 1, "audits serialize behind the gate — never two at once");
// Identical findings in every chunk, one merge PER chunk (jobs are
// independent — owner, 2026-09-29): 8 chunks × 2 propositions,
// 8 × 1 edge. The auditor's links, not the merger, dedupe identity.
// 8 × 1 chain. The auditor's links, not the merger, dedupe identity.
assert.deepEqual(stats, { nodes: 16, chains: 8 });
assert.equal(ledgers.readNodes().length, 16);
});
@@ -1802,7 +1743,6 @@ describe("the session-deletion guard (chat deleted mid-run)", () => {
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }, () => chatStore.deleteChat(chatId)),
});
const stats = await pipeline.runExtraction(chatId);
@@ -1827,118 +1767,57 @@ describe("the session-deletion guard (chat deleted mid-run)", () => {
});
});
// ─── the spine stage (owner, 2026-09-30): extract → spine → audit → merge ────
// ─── digest-aware serialized extraction + chain simplification (owner, 2026-09-30 evening) ────
describe("the spine stage (cross-stretch assembly)", () => {
it("the system prompt is goal-first: span doctrine + doctrines carried over, one shared core", () => {
assert.match(SPINE_CORE, /carry the source's line of argument ACROSS stretches/);
assert.match(SPINE_CORE, /may not all come from the same stretch/);
assert.match(SPINE_CORE, /JOINTLY entail the conclusion/);
assert.match(SPINE_CORE, /[Ee]ntailment is the\s+only link that earns a chain/);
assert.match(SPINE_CORE, /when in doubt, omit/);
assert.match(SPINE_CORE, /build your chains on\s+the EXISTING id, not the restatement/);
assert.match(SPINE_CORE, /SUBMIT with one submit_spine call/);
// Assembly: each front door = its preamble + the shared core.
assert.ok(SPINE_INSTRUCTIONS.includes(SPINE_CORE));
assert.ok(NOTES_SPINE_INSTRUCTIONS.includes(SPINE_CORE));
assert.match(SPINE_INSTRUCTIONS, /a teaching session/);
assert.match(NOTES_SPINE_INSTRUCTIONS, /the author's personal notes/);
describe("digest-aware serialized extraction (the extractor is the assembler)", () => {
it("the system prompt is goal-first: core ideas, foundations direction, build-on-the-digest doctrine", () => {
assert.match(EXTRACTION_CORE, /THE GOAL: uncover the source's CORE IDEAS and show how everything derives from the foundations/);
assert.match(EXTRACTION_CORE, /JOINTLY entail a conclusion/);
assert.match(EXTRACTION_CORE, /[Ee]ntailment is the\s+only link that earns a\s+chain/);
assert.match(EXTRACTION_CORE, /examples,\s+notation glosses, and restatements that no argument needs are not core ideas/);
assert.match(EXTRACTION_CORE, /build\s+on it\. Reference an existing idea by its id/);
assert.match(EXTRACTION_CORE, /OR existing ids from THE GRAPH SO FAR, verbatim/);
// Each front door = its preamble + the shared core; the preambles
// name the continuing-source posture.
assert.ok(EXTRACTOR_INSTRUCTIONS.includes(EXTRACTION_CORE));
assert.ok(NOTES_EXTRACTOR_INSTRUCTIONS.includes(EXTRACTION_CORE));
assert.match(SESSION_PREAMBLE, /building on everything already extracted from this source/);
assert.match(NOTES_PREAMBLE, /building on everything already extracted from this source/);
});
it("validateSpine enforces the span law: every chain joins fresh material to existing material", () => {
const chunkIds = new Set(["p1"]);
const existing = new Set(["n_a", "n_b"]);
const ok: SpineSubmit = { propositions: [], chains: [{ premises: ["n_a", "p1"], conclusion: "n_b" }] };
assert.equal(validateSpine(ok, chunkIds, existing), null);
// Connectives count as fresh; existing endpoints may sit anywhere.
const connective: SpineSubmit = {
propositions: [{ id: "s1", name: "C", statement: "c", aliases: [] }],
chains: [{ premises: ["s1", "p1"], conclusion: "n_a" }],
};
assert.equal(validateSpine(connective, chunkIds, existing), null);
// Empty submit is VALID — the honest common case (stretch stands alone).
assert.equal(validateSpine({ propositions: [], chains: [] }, chunkIds, existing), null);
// All-existing: the graph already had its chance — rejected.
assert.match(
validateSpine({ propositions: [], chains: [{ premises: ["n_a"], conclusion: "n_b" }] }, chunkIds, existing) ?? "",
/entirely existing material/,
);
// All-fresh: the stretch extractor's own work — rejected.
assert.match(
validateSpine(
{ propositions: [{ id: "s1", name: "C", statement: "c", aliases: [] }], chains: [{ premises: ["p1"], conclusion: "s1" }] },
chunkIds,
existing,
) ?? "",
/entirely inside the fresh stretch/,
);
// Unknown ids never pass silently.
assert.match(
validateSpine({ propositions: [], chains: [{ premises: ["p1"], conclusion: "zz" }] }, chunkIds, existing) ?? "",
/not an existing digest id/,
);
// Self-reference and id collisions are loud.
assert.match(
validateSpine({ propositions: [], chains: [{ premises: ["n_a", "p1"], conclusion: "p1" }] }, chunkIds, existing) ?? "",
/cannot be its own premise/,
);
assert.match(
validateSpine(
{ propositions: [{ id: "p1", name: "X", statement: "x", aliases: [] }], chains: [] },
chunkIds,
existing,
) ?? "",
/collides with an id this submission does not own/,
);
});
it("buildSpinePrompt renders the digest verbatim-id + the fresh stretch; spineHasDigest is same-source only", () => {
it("sourceDigest renders the same-source truth (ids verbatim) and stays empty for a barren source", () => {
const seed = (id: string, name: string, chatId: string): GraphNode => ({
id,
name,
statement: `statement of ${name}`,
aliases: [],
createdAt: "2026-09-30T00:00:00.000Z",
provenance: [{ chatId, messageId: "" }],
});
const chain = (id: string, premises: string[], conclusion: string, chatId: string): GraphChain => ({
id,
premises,
conclusion,
createdAt: "2026-09-30T00:00:01.000Z",
provenance: [{ chatId, messageId: "" }],
});
const graph = {
nodes: [
{
id: "n_a",
name: "Self-ownership",
statement: "s",
provenance: [{ chatId: "src_1" }],
},
{ id: "n_b", name: "Foreign", statement: "s", provenance: [{ chatId: "src_2" }] },
],
chains: [
{
id: "arg_x",
premises: ["n_a"],
conclusion: "n_b",
provenance: [{ chatId: "src_1" }],
},
],
} as unknown as Parameters<typeof buildSpinePrompt>[0];
const findings: SessionFindings = {
propositions: [{ id: "p1", name: "Fresh", statement: "f", aliases: [] }],
chains: [],
};
const prompt = buildSpinePrompt(graph, "src_1", findings);
assert.match(prompt, /THE GRAPH SO FAR/);
assert.match(prompt, /n_a \| Self-ownership/);
assert.doesNotMatch(prompt, /n_b \| Foreign/); // other sources' nodes stay out
assert.match(prompt, /\[n_a ⟹ n_b\]/); // recorded chains shown as ids
assert.match(prompt, /THE FRESH STRETCH/);
assert.match(prompt, /p1 \| Fresh/);
// The chain line names the foreign conclusion via the id-name map —
// ids always render, names come from anywhere in the graph.
assert.match(prompt, /Self-ownership ⟹ Foreign/);
assert.equal(spineHasDigest(graph, "src_1"), true);
assert.equal(spineHasDigest(graph, "src_9"), false);
nodes: [seed("n_a", "Self-ownership", "src_1"), seed("n_x", "Foreign key", "src_2")],
chains: [chain("arg_1", ["n_a"], "n_x", "src_2")], // same NAME, other source — excluded
sessions: {},
sources: {},
} as unknown as Parameters<typeof sourceDigest>[0];
assert.equal(sourceDigest(graph, "src_9"), "", "a source owning nothing runs bare — no digest block");
const digest = sourceDigest(graph, "src_1");
assert.match(digest, /THE GRAPH SO FAR/);
assert.match(digest, /n_a \| Self-ownership \| statement of Self-ownership/);
assert.doesNotMatch(digest, /Foreign key/, "other sources' material never leaks into the digest");
assert.doesNotMatch(digest, /DERIVATIONS ALREADY RECORDED/, "no same-source chains → no chain block");
});
it("a seeded digest joins a fresh stretch: the spanning chain + connective ride the audit and merge, billed as the spine stage", async () => {
it("a seeded digest rides the extractor's view: mixed-namespace chains reference existing ids VERBATIM", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
// A PREVIOUS stretch's residue: two nodes already in the graph,
// provenance on this chat — the spine's digest.
const seed = (id: string, name: string): GraphNode => ({
id,
name,
@@ -1948,127 +1827,221 @@ describe("the spine stage (cross-stretch assembly)", () => {
provenance: [{ chatId, messageId: "m0" }],
});
ledgers.appendNode(seed("n_a", "Self-ownership"));
ledgers.appendNode(seed("n_b", "Scarcity"));
const FINDINGS_B: SessionFindings = {
propositions: [{ id: "p1", name: "First-use survives", statement: "c", aliases: [] }],
chains: [],
propositions: [{ id: "p1", name: "Appropriation axiom", statement: "c", aliases: [] }],
chains: [{ premises: ["n_a", "p1"], conclusion: "p1", }].slice(0, 0), // chains live in the engine below
};
const spineViews: string[] = [];
let spineCalls = 0;
const phases: string[] = [];
type LiveUsage = NonNullable<ReturnType<typeof extractionStatuses>[number]["usage"]>;
const usages: LiveUsage[] = [];
const spineEngine = fauxEngine([
const views: string[] = [];
const extractor = fauxEngine([
(context) => {
spineCalls++;
spineViews.push(capturedView(context));
phases.push(extractionStatuses().find((r) => r.chatId === chatId)?.phase ?? "?");
const submit: SpineSubmit = {
propositions: [{ id: "s1", name: "Appropriation axiom", statement: "d", aliases: [] }],
chains: [{ premises: ["n_a", "p1"], conclusion: "s1" }],
views.push(capturedView(context));
const submit = {
propositions: FINDINGS_B.propositions,
chains: [{ premises: ["n_a", "p1"], conclusion: "p1b" }],
};
return fauxAssistantMessage([fauxToolCall("submit_spine", submit as unknown as ToolCall["arguments"], { id: "sp1" })], { stopReason: "toolUse" });
// conclusion p1b must exist — real submit:
submit.propositions.push({ id: "p1b", name: "Property rights", statement: "d", aliases: [] });
return fauxAssistantMessage([fauxToolCall("submit_findings", submit as unknown as ToolCall["arguments"], { id: "f1" })], { stopReason: "toolUse" });
},
]);
const reviewMessage = () =>
fauxAssistantMessage([fauxToolCall("submit_review", { links: [] } as unknown as ToolCall["arguments"], { id: "rv" })], { stopReason: "toolUse" });
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS_B),
spine: spineEngine,
auditor: fauxEngine([
() => {
usages.push(extractionStatuses().find((r) => r.chatId === chatId)?.usage ?? {});
return reviewMessage();
},
]),
});
const stats = await pipeline.runExtraction(chatId);
// The spine ran once, under its own phase, seeing the digest and the
// fresh stretch rendered together (existing ids verbatim).
assert.equal(spineCalls, 1);
assert.deepEqual(phases, ["assembling"]);
assert.match(spineViews[0]!, /THE GRAPH SO FAR/);
assert.match(spineViews[0]!, /n_a \| Self-ownership/);
assert.match(spineViews[0]!, /p1 \| First-use survives/);
// 2 nodes this run (the fresh proposition + the connective), 1 spanning chain.
assert.deepEqual(stats, { nodes: 2, chains: 1 });
const stored = ledgers.readChains();
assert.equal(stored.length, 1);
const nodes = ledgers.readNodes();
const byName = new Map(nodes.map((n) => [n.name, n.id]));
// Mixed namespace resolved: the existing n_a passes through
// VERBATIM; fresh p1/s1 resolve through the job-local map.
assert.deepEqual(stored[0]!.premises, ["n_a", byName.get("First-use survives")]);
assert.equal(stored[0]!.conclusion, byName.get("Appropriation axiom"));
// Billing: the spine is its own stage — one extractor step, one
// spine step, one auditor step, live in the registry.
assert.equal(usages[0]!.extractor?.steps.length, 1);
assert.equal(usages[0]!.spine?.steps.length, 1);
});
it("a non-spanning spine submit is REFUSED with the problem and retried: only the spanning retry merges", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
const seed = (id: string, name: string): GraphNode => ({
id,
name,
statement: `statement of ${name}`,
aliases: [],
createdAt: "2026-09-30T00:00:00.000Z",
provenance: [{ chatId, messageId: "m0" }],
});
ledgers.appendNode(seed("n_a", "Self-ownership"));
ledgers.appendNode(seed("n_b", "Scarcity"));
const FINDINGS_B: SessionFindings = {
propositions: [{ id: "p1", name: "First-use survives", statement: "c", aliases: [] }],
chains: [],
};
const spineEngine = fauxEngine([
// All-existing chain: the graph already had its chance — refused
// with the problem, error-back to the model.
() =>
fauxAssistantMessage(
[
fauxToolCall(
"submit_spine",
{ propositions: [], chains: [{ premises: ["n_a"], conclusion: "n_b" }] } as unknown as ToolCall["arguments"],
{ id: "sp-bad" },
),
],
{ stopReason: "toolUse" },
),
// The retry spans: existing n_a + fresh p1 ⟹ existing n_b.
() =>
fauxAssistantMessage(
[
fauxToolCall(
"submit_spine",
{ propositions: [], chains: [{ premises: ["n_a", "p1"], conclusion: "n_b" }] } as unknown as ToolCall["arguments"],
{ id: "sp-good" },
),
],
{ stopReason: "toolUse" },
),
]);
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS_B),
spine: spineEngine,
extractor,
auditor: submitModel("submit_review", { links: [] }),
});
const stats = await pipeline.runExtraction(chatId);
// The refused attempt left nothing; the retry's spanning chain merged.
assert.deepEqual(stats, { nodes: 1, chains: 1 });
// The extractor's view LED with the digest (existing id verbatim).
assert.match(views[0]!, /THE GRAPH SO FAR/);
assert.match(views[0]!, /n_a \| Self-ownership/);
// Mixed namespace resolved: n_a passes through verbatim; p1/p1b
// resolve through the job-local map.
assert.deepEqual(stats, { nodes: 2, chains: 1 });
const stored = ledgers.readChains();
assert.equal(stored.length, 1);
const firstUse = ledgers.readNodes().find((n) => n.name === "First-use survives")!;
assert.deepEqual(stored[0]!.premises, ["n_a", firstUse.id]);
assert.equal(stored[0]!.conclusion, "n_b");
const byName = new Map(ledgers.readNodes().map((n) => [n.name, n.id]));
assert.deepEqual(stored[0]!.premises, ["n_a", byName.get("Appropriation axiom")]);
assert.equal(stored[0]!.conclusion, byName.get("Property rights"));
});
it("a chain endpoint that resolves NOWHERE is refused with the problem and the retry merges (error-back)", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
const badThenGood = fauxEngine([
() =>
fauxAssistantMessage(
[
fauxToolCall(
"submit_findings",
{ propositions: [], chains: [{ premises: ["n_nope"], conclusion: "also_nope" }] } as unknown as ToolCall["arguments"],
{ id: "bad" },
),
],
{ stopReason: "toolUse" },
),
() =>
fauxAssistantMessage(
[
fauxToolCall(
"submit_findings",
{
propositions: [{ id: "p1", name: "Axiom", statement: "s", aliases: [] }],
chains: [],
} as unknown as ToolCall["arguments"],
{ id: "good" },
),
],
{ stopReason: "toolUse" },
),
]);
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
extractor: badThenGood,
auditor: submitModel("submit_review", { links: [] }),
});
const stats = await pipeline.runExtraction(chatId);
assert.deepEqual(stats, { nodes: 1, chains: 0 }, "the refused attempt left nothing; the retry merged");
});
it("batches serialize: chunk 2's extractor starts only after chunk 1's audit ran (digest order, owner 2026-09-30 evening)", async () => {
const { chatId } = chunkyChat(); // 2 deterministic batches at the 4096 override
const ledgers = memoryLedgers();
const events: string[] = [];
let extractCalls = 0;
let auditCalls = 0;
// One step per engine call (faux engines consume steps): two batches.
const extractStep = (): FauxResponseStep => () => {
extractCalls++;
events.push(`extract${extractCalls}`);
return fauxAssistantMessage(
[fauxToolCall("submit_findings", FINDINGS as unknown as ToolCall["arguments"], { id: `f${extractCalls}` })],
{ stopReason: "toolUse" },
);
};
const auditStep = (): FauxResponseStep => () => {
auditCalls++;
events.push(`audit${auditCalls}`);
return fauxAssistantMessage(
[fauxToolCall("submit_review", { links: [] } as unknown as ToolCall["arguments"], { id: `r${auditCalls}` })],
{ stopReason: "toolUse" },
);
};
const extractor = fauxEngine([extractStep(), extractStep()]);
const auditor = fauxEngine([auditStep(), auditStep()]);
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
extractor,
auditor,
getChunkTokens: () => 4096, // two batches over the chunky fixture (factory default doubled to 8192, 2026-09-30)
});
const stats = await pipeline.runExtraction(chatId);
assert.deepEqual(events, ["extract1", "audit1", "extract2", "audit2"], "strict per-source order: extract → audit → merge → NEXT extract");
assert.equal(stats.nodes, 4, "both batches merged (2 propositions each)");
});
});
describe("chain simplification (the merger's maintenance pass, owner 2026-09-30 evening)", () => {
const chain = (id: string, premises: string[], conclusion: string, createdAt: string): GraphChain => ({
id,
premises,
conclusion,
createdAt,
provenance: [{ chatId: "src_1", messageId: "" }],
});
it("the owner example: [A,B]⇒X + [X,C]⇒Y make [A,B,C]⇒Y redundant (ancestry-cover)", () => {
const kept = simplifyChains([
chain("arg_3", ["n_a", "n_b", "n_c"], "n_y", "2026-09-30T03:00:00.000Z"),
chain("arg_1", ["n_a", "n_b"], "n_x", "2026-09-30T01:00:00.000Z"),
chain("arg_2", ["n_x", "n_c"], "n_y", "2026-09-30T02:00:00.000Z"),
]);
assert.deepEqual(kept.map((c) => c.id).sort(), ["arg_1", "arg_2"]);
});
it("prefer-fewest-premises: a strict premise-superset of a kept chain drops", () => {
const kept = simplifyChains([
chain("arg_1", ["n_a"], "n_z", "2026-09-30T01:00:00.000Z"),
chain("arg_2", ["n_a", "n_b"], "n_z", "2026-09-30T02:00:00.000Z"),
]);
assert.deepEqual(kept.map((c) => c.id), ["arg_1"]);
});
it("genuinely different routes to one conclusion stand; a superset route yields to the lean one", () => {
const kept = simplifyChains([
chain("arg_1", ["n_a"], "n_y", "2026-09-30T01:00:00.000Z"),
chain("arg_2", ["n_b"], "n_y", "2026-09-30T02:00:00.000Z"),
chain("arg_3", ["n_c", "n_d"], "n_y", "2026-09-30T03:00:00.000Z"),
chain("arg_4", ["n_a", "n_c"], "n_y", "2026-09-30T04:00:00.000Z"),
]);
// arg_3's premises are covered by NO kept chain — an independent
// route, it stands. arg_4 is a strict premise-superset of arg_1's
// route (n_a already suffices) — the lean derivation wins.
assert.deepEqual(kept.map((c) => c.id).sort(), ["arg_1", "arg_2", "arg_3"]);
});
it("ancestry reaches transitively: [W]⇒A chains through [A,B]⇒X", () => {
const kept = simplifyChains([
chain("arg_1", ["n_w"], "n_a", "2026-09-30T01:00:00.000Z"),
chain("arg_2", ["n_a", "n_b"], "n_x", "2026-09-30T02:00:00.000Z"),
chain("arg_3", ["n_x", "n_c"], "n_y", "2026-09-30T03:00:00.000Z"),
chain("arg_4", ["n_w", "n_b", "n_c"], "n_y", "2026-09-30T04:00:00.000Z"),
]);
assert.deepEqual(kept.map((c) => c.id).sort(), ["arg_1", "arg_2", "arg_3"]);
});
it("same-key provenance lines drop TOGETHER (a key is kept or dropped as a whole)", () => {
const kept = simplifyChains([
chain("arg_1", ["n_a"], "n_z", "2026-09-30T01:00:00.000Z"),
{ ...chain("arg_1", ["n_a"], "n_z", "2026-09-30T01:00:00.000Z"), provenance: [{ chatId: "src_2", messageId: "" }] },
chain("arg_2", ["n_a", "n_b"], "n_z", "2026-09-30T02:00:00.000Z"),
{ ...chain("arg_2", ["n_a", "n_b"], "n_z", "2026-09-30T02:00:00.000Z"), provenance: [{ chatId: "src_2", messageId: "" }] },
]);
assert.deepEqual(kept.map((c) => c.id), ["arg_1", "arg_1"], "the superset key's every line went, the kept key's every line stayed");
});
it("the merge applies it live: a redundant arrival is skipped silently, stats count appends and drops", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
// Seed [A,B]⇒X and [X,C]⇒Y as an earlier source's chains.
const seedNode = (id: string, name: string): GraphNode => ({
id,
name,
statement: `statement of ${name}`,
aliases: [],
createdAt: "2026-09-30T00:00:00.000Z",
provenance: [{ chatId, messageId: "m0" }],
});
for (const [id, name] of [
["n_a", "A"],
["n_b", "B"],
["n_c", "C"],
["n_x", "X"],
["n_y", "Y"],
] as const)
ledgers.appendNode(seedNode(id, name));
ledgers.appendChain(chain("arg_1", ["n_a", "n_b"], "n_x", "2026-09-30T01:00:00.000Z"));
ledgers.appendChain(chain("arg_2", ["n_x", "n_c"], "n_y", "2026-09-30T02:00:00.000Z"));
// The chat re-asserts the full derivation [A,B,C]⇒Y (all-existing
// ids, digest references) — redundant given what stands.
const redundant: SessionFindings = {
propositions: [],
chains: [{ premises: ["n_a", "n_b", "n_c"], conclusion: "n_y" }],
};
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", redundant),
auditor: submitModel("submit_review", { links: [] }),
});
const stats = await pipeline.runExtraction(chatId);
assert.equal(stats.chains, 1, "the append happened (stats count activity)");
assert.equal(stats.droppedChains, 1, "the simplification pass then removed it (maintenance)");
const stored = ledgers.readChains();
assert.equal(stored.length, 2, "the ledger is back to the two lean chains");
});
});
-1
View File
@@ -54,7 +54,6 @@ describe("displayCostTotals (naming-free cost fold)", () => {
const costs = {
stages: {
extraction: stage(1.25, 1),
spine: stage(0), // spine channel exists since 2026-09-30 — zero spend here
audit: stage(0.75),
naming: stage(9.99, 2), // dead channel — must never reach the display
},
+1 -1
View File
@@ -49,7 +49,7 @@ describe("parseAgentSettingsBody", () => {
});
const bad = parseAgentSettingsBody({ models: { teacher: {} } });
assert.equal(bad.ok, false);
if (!bad.ok) assert.match(bad.error, /unknown role "teacher" \(supported: researcher, demoBuilder, visualMaker, extractor, spine, auditor, partitioner, problemGenerator, grader\)/);
if (!bad.ok) assert.match(bad.error, /unknown role "teacher" \(supported: researcher, demoBuilder, visualMaker, extractor, auditor, partitioner, problemGenerator, grader\)/);
const malformed = parseAgentSettingsBody({ models: { researcher: { model: { provider: 1, id: "x" } } } });
assert.equal(malformed.ok, false);
});
+2 -3
View File
@@ -23,7 +23,7 @@ describe("FACTORY_SETTINGS (the compiled factory layer)", () => {
assert.deepEqual(FACTORY_SETTINGS.general, { autoUpdate: true });
// wave 7 collapse: the master switch ONLY + the extraction-UX
// redesign's chunk budget (2024-09-24); default doubled 8192 with
// the spine stage (owner, 2026-09-30)
// the digest-aware redesign (owner, 2026-09-30)
assert.deepEqual(FACTORY_SETTINGS.graph, { enabled: true, extractionChunkTokens: 8192 });
const a = FACTORY_SETTINGS.appearance;
assert.equal(a.mode, "dark");
@@ -203,13 +203,12 @@ describe("sanitizeUserSettings", () => {
assert.deepEqual(out.agent?.tools, { quiz: false, demo: true });
});
it("BACKGROUND_MODEL_ROLES matches the nine background roles (spine added 2026-09-30; practice trio 2026-09-29)", () => {
it("BACKGROUND_MODEL_ROLES matches the eight background roles (practice trio 2026-09-29)", () => {
assert.deepEqual([...BACKGROUND_MODEL_ROLES], [
"researcher",
"demoBuilder",
"visualMaker",
"extractor",
"spine",
"auditor",
"partitioner",
"problemGenerator",
-1
View File
@@ -127,7 +127,6 @@ const catalog = (strongProvider: string | null, ...keyedProviders: string[]): Mo
demoBuilder: null,
visualMaker: null,
extractor: null,
spine: null,
auditor: null,
partitioner: null,
problemGenerator: null,