extraction: the spine stage — cross-stretch assembly between extract and audit

Per-batch pipeline is now extract → spine → audit → merge (owner order,
2026-09-30), the spine inside the serialized audit section so it reads
the graph exactly as the following audit will. Measured motive: 0 of 133
chains in the live corpus crossed a stretch boundary — no stage minted
cross-stretch chains.

- src/agent/subagents/spine.ts: SESSION/NOTES spine preambles + shared
  SPINE_CORE (span doctrine; extraction doctrines carried over), the
  submit_spine TypeBox schema, validateSpine (mechanical span law: every
  chain touches >=1 fresh endpoint AND >=1 existing digest endpoint;
  unknown ids, self-reference, and id collisions are loud), the digest
  prompt builder, and the runner (strong tier, retry-only).
- extraction.ts: 'assembling' phase, spine stage in the per-need gate,
  usage/billing stage, assemble() inside withAuditLock for both front
  doors; first stretch of a source skips the call (empty digest, no
  spend); the audit sees COMBINED propositions (chunk + connectives),
  never chains; merger receives the combined findings.
- core/graph.ts mergeFindings: chain endpoints resolve through the
  job-local map OR pass through verbatim as existing n_ ids (the spine's
  mixed namespace).
- EXTRACTION_CORE: steps-of-reasoning selection bar (examples, notation
  glosses, and restatements that no argument needs are not steps).
- Factory chunk budget 4096 -> 8192 (fewer seams, more local context).
- healStringifiedArrays moved to the shared submit seam (submit-agent.ts).
- Cost panel: Assembly row; usage records carry a spine stage (optional
  on legacy records).
- Tests +5: prompt pins, span-law matrix, digest rendering, seeded
  end-to-end flow (spanning chain + connective, verbatim n_ endpoint,
  spine billed as its own stage), non-spanning refused + retried.
This commit is contained in:
amosblomqvist
2026-09-30 16:38:45 +03:00
parent 5ed724c224
commit 52c02533d1
18 changed files with 787 additions and 87 deletions
+336 -13
View File
@@ -52,6 +52,15 @@ import {
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 { 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";
@@ -275,6 +284,15 @@ 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 () => {
@@ -384,7 +402,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chains: FINDINGS.chains,
});
const pipeline = createExtractionPipeline({ chatStore, graphStore: ledgers, extractor, auditor: submitModel("submit_review", verdict) });
const pipeline = createExtractionPipeline({ chatStore, graphStore: ledgers, extractor, spine: spineModel(), auditor: submitModel("submit_review", verdict) });
const stats = await pipeline.runExtraction(chatId);
assert.deepEqual(stats, { nodes: 2, chains: 1 });
});
@@ -400,6 +418,7 @@ 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 });
@@ -428,6 +447,7 @@ 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);
@@ -476,7 +496,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"]); // extractor + auditor are the cheap tier
assert.deepEqual(err.envelope.missing, ["cheap", "strong"]); // extractor + auditor cheap, spine strong
assert.equal(err.envelope.code, "models_not_configured");
return true;
});
@@ -494,6 +514,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor,
spine: spineModel(),
auditor,
providerKeyMissing: (provider, tier) => {
seen.push([provider, tier]);
@@ -521,14 +542,15 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: fauxEngine([]),
spine: spineModel(),
auditor: fauxEngine([]),
providerKeyMissing: (provider, tier) => (calls++ === 0 ? null : providerKeyMissing(provider, [tier])),
providerKeyMissing: (provider, tier) => (calls++ < 2 ? null : providerKeyMissing(provider, [tier])),
});
await assert.rejects(pipeline.runExtraction(chatId), (err: unknown) => {
assert.ok(err instanceof ProviderKeyMissingError, "the auditor's refusal surfaces after the extractor passed");
assert.ok(err instanceof ProviderKeyMissingError, "the auditor's refusal surfaces after extractor + spine passed");
return true;
});
assert.equal(calls, 2, "both stages were checked, in order");
assert.equal(calls, 3, "all three stages were checked, in order");
assert.equal(ledgers.readCoverage().length, 0);
});
@@ -541,6 +563,7 @@ 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);
@@ -566,17 +589,17 @@ 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: [] });
},
});
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 (the
// labeler is fire-and-forget garnish — extractor + auditor are the
// pinned order).
assert.equal(consulted[0], "extractor");
assert.equal(consulted[1], "auditor");
// The stages were resolved through the chain, in pipeline order
// (extractor → spine → auditor; the labeler is fire-and-forget
// garnish for any other role).
assert.deepEqual(consulted.slice(0, 3), ["extractor", "spine", "auditor"]);
});
it("direct test values still WIN over engineFor (the priority pin)", async () => {
@@ -587,6 +610,7 @@ 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) => {
@@ -605,6 +629,7 @@ 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
@@ -618,6 +643,7 @@ 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 });
@@ -639,8 +665,15 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const mixed = createExtractionPipeline({
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).
getChunkTokens: () => 4096,
engineFor: (role) =>
role === "extractor" ? fauxEngine([countingStep]) : submitModel("submit_review", { links: [] }),
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");
assert.equal(invocations, 2);
@@ -675,6 +708,7 @@ 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);
@@ -701,6 +735,7 @@ 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;
}),
@@ -718,6 +753,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.
spine: spineModel(),
auditor: submitModel("submit_review", { links: [] }, () => {
bAuditorCalled = true;
bSawAlphasMerge = ledgers.readNodes().some((n) => n.name === "Alpha claim");
@@ -755,6 +791,7 @@ 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);
@@ -765,6 +802,7 @@ 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;
}),
@@ -787,6 +825,7 @@ 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 });
@@ -814,6 +853,7 @@ 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),
),
@@ -852,6 +892,7 @@ 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);
@@ -902,6 +943,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: ledgers,
extractor: gatedExtractor,
spine: spineModel(),
auditor: silentModel(),
});
@@ -938,6 +980,7 @@ 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
@@ -947,6 +990,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
chatStore,
graphStore: memoryLedgers(),
extractor: silentModel(),
spine: spineModel(),
auditor: silentModel(),
});
@@ -973,6 +1017,7 @@ 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/);
@@ -1042,6 +1087,7 @@ 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 });
@@ -1077,6 +1123,7 @@ 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;
@@ -1097,6 +1144,7 @@ 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 });
@@ -1141,6 +1189,7 @@ 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;
@@ -1159,6 +1208,7 @@ 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 });
@@ -1175,6 +1225,7 @@ 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 });
@@ -1223,6 +1274,7 @@ 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 });
@@ -1282,7 +1334,9 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
engineFor: (role) =>
role === "extractor"
? extractorStep()
: submitModel("submit_review", { links: [] }),
: role === "spine"
? submitModel("submit_spine", { propositions: [], chains: [] })
: submitModel("submit_review", { links: [] }),
getChunkTokens: () => 20, // 80 chars/line-groups → several chunks
});
const stats = await pipeline.importNotes({ filePath: "/vault/big.md", text });
@@ -1335,6 +1389,7 @@ 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 });
@@ -1365,6 +1420,7 @@ 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 });
@@ -1445,7 +1501,11 @@ describe("the chunked pipeline (extraction coverage)", () => {
const pipeline = createExtractionPipeline({
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).
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);
@@ -1493,6 +1553,9 @@ describe("the chunked pipeline (extraction coverage)", () => {
const pipeline = createExtractionPipeline({
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).
getChunkTokens: () => 4096,
// Batch 1 submits and merges; batch 2's extractor call HANGS
// until the abort fires — the dock's abort button, mid-run.
extractor: fauxEngine([
@@ -1507,6 +1570,7 @@ describe("the chunked pipeline (extraction coverage)", () => {
sig?.addEventListener("abort", () => resolve(fauxAssistantMessage("late")), { once: true });
}),
]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep()]),
});
const runPromise = pipeline.runExtraction(chatId);
@@ -1537,6 +1601,7 @@ 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);
@@ -1561,6 +1626,7 @@ describe("the chunked pipeline (extraction coverage)", () => {
chatStore,
graphStore: ledgers,
extractor: fauxEngine([findingsStep()]),
spine: spineModel(),
auditor: fauxEngine([passThroughAuditStep()]),
});
await pipeline.runExtraction(chatId);
@@ -1596,6 +1662,7 @@ 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);
@@ -1697,10 +1764,19 @@ 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]) : fauxEngine([slowAudit])),
engineFor: (role) =>
role === "extractor"
? fauxEngine([slowExtract])
: role === "spine"
? fauxEngine([() => spineMessage])
: fauxEngine([slowAudit]),
getChunkTokens: () => 20,
});
const stats = await pipeline.importNotes({ filePath: "/vault/parallel-2.md", text: wideText() });
@@ -1726,6 +1802,7 @@ 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);
@@ -1749,3 +1826,249 @@ describe("the session-deletion guard (chat deleted mid-run)", () => {
assert.equal(chatStore.getChat(chatId), null);
});
});
// ─── the spine stage (owner, 2026-09-30): extract → spine → audit → merge ────
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/);
});
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", () => {
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);
});
it("a seeded digest joins a fresh stretch: the spanning chain + connective ride the audit and merge, billed as the spine stage", 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,
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 spineViews: string[] = [];
let spineCalls = 0;
const phases: string[] = [];
type LiveUsage = NonNullable<ReturnType<typeof extractionStatuses>[number]["usage"]>;
const usages: LiveUsage[] = [];
const spineEngine = 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" }],
};
return fauxAssistantMessage([fauxToolCall("submit_spine", submit as unknown as ToolCall["arguments"], { id: "sp1" })], { 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,
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 });
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");
});
});
+2 -1
View File
@@ -54,12 +54,13 @@ 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
},
} as const;
it("sums extraction + audit only — the dead namer's spend is invisible", () => {
it("sums the live stages only — the dead namer's spend is invisible", () => {
assert.equal(displayCostTotals(costs).totalDollars, 2);
});
+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, auditor, partitioner, problemGenerator, grader\)/);
if (!bad.ok) assert.match(bad.error, /unknown role "teacher" \(supported: researcher, demoBuilder, visualMaker, extractor, spine, auditor, partitioner, problemGenerator, grader\)/);
const malformed = parseAgentSettingsBody({ models: { researcher: { model: { provider: 1, id: "x" } } } });
assert.equal(malformed.ok, false);
});
+4 -4
View File
@@ -42,8 +42,8 @@ describe("resolveSettings", () => {
assert.equal(eff.appearance.sessionNameSerif, true);
assert.equal(eff.appearance.mdRendering.headingWeight, 900);
// wave 7 collapse: the master switch ONLY + the redesign's chunk
// budget — absent record resolves the factory 4096 (the old config constant)
assert.deepEqual(eff.graph, { enabled: true, extractionChunkTokens: 4096 });
// budget — absent record resolves the factory 8192 (the 2026-09-30 bump)
assert.deepEqual(eff.graph, { enabled: true, extractionChunkTokens: 8192 });
});
it("explicit record values win over every factory default", () => {
@@ -75,8 +75,8 @@ describe("resolveSettings", () => {
assert.equal(eff.appearance.proseFontSize, 20);
assert.equal(eff.appearance.mdRendering.headingWeight, 600);
// wave 7 collapse: the section IS the master switch (absent budget
// field = factory 4096 — a pre-redesign record resolves cleanly).
assert.deepEqual(eff.graph, { enabled: false, extractionChunkTokens: 4096 });
// field = factory 8192 — a pre-redesign record resolves cleanly).
assert.deepEqual(eff.graph, { enabled: false, extractionChunkTokens: 8192 });
// The chunk budget rides the same resolution: stored value wins.
const budgeted = resolveSettings({ version: 1, graph: { extractionChunkTokens: 2000 } });
assert.deepEqual(budgeted.graph, { enabled: true, extractionChunkTokens: 2000 });
+5 -3
View File
@@ -22,8 +22,9 @@ describe("FACTORY_SETTINGS (the compiled factory layer)", () => {
assert.equal(CURRENT_SETTINGS_VERSION, 1);
assert.deepEqual(FACTORY_SETTINGS.general, { autoUpdate: true });
// wave 7 collapse: the master switch ONLY + the extraction-UX
// redesign's chunk budget (2026-09-24) — default = the old config constant
assert.deepEqual(FACTORY_SETTINGS.graph, { enabled: true, extractionChunkTokens: 4096 });
// redesign's chunk budget (2024-09-24); default doubled 8192 with
// the spine stage (owner, 2026-09-30)
assert.deepEqual(FACTORY_SETTINGS.graph, { enabled: true, extractionChunkTokens: 8192 });
const a = FACTORY_SETTINGS.appearance;
assert.equal(a.mode, "dark");
assert.equal(a.background, null); // the mode's own default, not a factory hex
@@ -202,12 +203,13 @@ describe("sanitizeUserSettings", () => {
assert.deepEqual(out.agent?.tools, { quiz: false, demo: true });
});
it("BACKGROUND_MODEL_ROLES matches the eight background roles (practice trio added, owner 2026-09-29)", () => {
it("BACKGROUND_MODEL_ROLES matches the nine background roles (spine added 2026-09-30; practice trio 2026-09-29)", () => {
assert.deepEqual([...BACKGROUND_MODEL_ROLES], [
"researcher",
"demoBuilder",
"visualMaker",
"extractor",
"spine",
"auditor",
"partitioner",
"problemGenerator",
+1
View File
@@ -127,6 +127,7 @@ const catalog = (strongProvider: string | null, ...keyedProviders: string[]): Mo
demoBuilder: null,
visualMaker: null,
extractor: null,
spine: null,
auditor: null,
partitioner: null,
problemGenerator: null,