feat(extraction): stage engines resolve the slot chain — engineFor on the pipeline
Chunk B, item 3 of the search port + slots wave (2026-09-20): the flagged gap closed — ExtractionModels deps were engine VALUES, keeping extractor/auditor/labeler config-pin-bound while the job runner went slot-native. The pipeline now takes the runner's exact treatment: - ExtractionPipelineDeps.engineFor: (role) => PiEngine — resolved FRESH per stage call, so a settings PUT between stages lands. Direct test values (extractor/auditor/labeler) remain the priority seam, checked FIRST; absent values fall to the chain; absent chain → config pins (unchanged). - All six stage call sites (chat + notes extract/audit, the cluster namer x2) resolve through stageEngine(role, direct). - chat-handler's extraction composition injects the full chain — piEngineForSpec(resolveRoleSpec(role, readSettings(), MODEL_PINS)) — same wiring as the job runner. Chunk B, item 4 (session-level slot-anchored model picks): VERIFIED already complete from Platform's model-slots wave — the conversation engine chain (chat-handler) anchors on effective.models.strong, the picker's default surface (catalogDefaultsFor) falls back to strong, both pinned in tests. No phase split (user decision, 2026-09-20). Capability-filtered pickers live in the settings UI (main 343be10). Tests: engineFor fills the stages end-to-end (both propositions merged through the chain, consulted in pipeline order) + the direct-value priority pin (engineFor that throws when consulted). Gates: typecheck, 795/795 tests, lint, build — all green.
This commit is contained in:
@@ -401,6 +401,64 @@ 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: AuditResult = {
|
||||
propositions: FINDINGS.propositions,
|
||||
edges: [], // the edge is dropped with it
|
||||
attribution: FINDINGS.attribution,
|
||||
dropped: [{ ref: "p1→p2", reason: "endpoint dropped" }],
|
||||
};
|
||||
const consulted: string[] = [];
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
// NO direct values — the stages resolve ENTIRELY through engineFor
|
||||
// (the role override → slot → factory-pin chain, read fresh per
|
||||
// stage call in production; here, the scripted chain).
|
||||
engineFor: (role) => {
|
||||
consulted.push(role);
|
||||
if (role === "extractor") return submitModel("submit_findings", FINDINGS);
|
||||
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");
|
||||
});
|
||||
|
||||
it("direct test values still WIN over engineFor (the priority pin)", async () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
const verdict: AuditResult = {
|
||||
propositions: FINDINGS.propositions,
|
||||
edges: [],
|
||||
attribution: FINDINGS.attribution,
|
||||
dropped: [{ ref: "p1→p2", reason: "endpoint dropped" }],
|
||||
};
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: submitModel("submit_review", verdict),
|
||||
labeler: submitModel("submit_labels", { labels: [] }),
|
||||
// If the direct values did NOT win, this throw would surface.
|
||||
engineFor: (role) => {
|
||||
throw new Error(`engineFor should not be consulted for ${role} when a direct value is set`);
|
||||
},
|
||||
});
|
||||
const stats = await pipeline.runExtraction(chatId);
|
||||
assert.equal(stats.nodes, 2, "the direct-value seams ran, untouched by engineFor");
|
||||
});
|
||||
|
||||
it("checkpoints nothing when a stage fails", async () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
|
||||
Reference in New Issue
Block a user