push architecture slice 5: extraction/practice registries announce activity-changed; dock subscribes (debounced) — the adaptive poll and every chat-view dock nudge retire

This commit is contained in:
amosblomqvist
2026-10-02 17:31:35 +03:00
parent a38c400eb7
commit d6450e260a
6 changed files with 164 additions and 102 deletions
+31
View File
@@ -57,6 +57,7 @@ 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";
import { subscribe } from "../src/core/events.ts";
import { PATHS } from "../config.ts";
// ─── fixtures ────────────────────────────────────────────────────────────────
@@ -372,6 +373,36 @@ describe("transcriptView (pure — teacher prose only, owner 2026-09-28)", () =>
// ─── the pipeline ───────────────────────────────────────────────────────────
describe("the extraction pipeline (extractor → auditor → merger)", () => {
it("push S5: every registry mutation announces activity-changed; reads stay silent", async () => {
const { chatId } = fixtureChat();
const ledgers = memoryLedgers();
const verdict: AuditVerdicts = { links: [] };
const pipeline = createExtractionPipeline({
chatStore,
graphStore: ledgers,
extractor: submitModel("submit_findings", FINDINGS),
auditor: submitModel("submit_review", verdict),
});
let announces = 0;
const off = subscribe((e) => {
if (e.type === "activity-changed") announces++;
});
// A read announces nothing (reads are silent — the bus hygiene law).
extractionStatuses();
assert.equal(announces, 0);
await pipeline.runExtraction(chatId);
// The run announced its lifecycle: setPhase at registration and every
// phase transition, finishExtraction at the exit — several events,
// and NONE after settle (the registry is empty; nothing left to say).
assert.ok(announces >= 2, `phases + finish announced (got ${announces})`);
const afterSettle = announces;
extractionStatuses();
assert.equal(announces, afterSettle, "reads stay silent");
off();
});
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();
+39
View File
@@ -26,6 +26,7 @@ import {
BadAnswersError,
type RunningPractice,
} from "../src/agent/subagents/practice-pipeline.ts";
import { subscribe } from "../src/core/events.ts";
import { chainCoverage } from "../src/core/practice.ts";
import type { GraphChain, GraphNode, ProblemSet } from "../src/core/contracts.ts";
import type { ChatStore, GraphStore, PracticeStore } from "../src/core/ports.ts";
@@ -403,6 +404,44 @@ describe("createProblemSet (generation)", () => {
assert.equal(practiceStatuses().length, 0);
});
it("push S5: registerRow/phase-writes/finishPractice all announce activity-changed; reads stay silent", async () => {
const factory: FauxResponseStep = () =>
fauxAssistantMessage(
[
fauxToolCall(
"submit_problem",
{ chains: ["b"], statement: "s", solution: "v", rubric: "r" } as ToolCall["arguments"],
{ id: "gen-b" },
),
],
{ stopReason: "toolUse" },
);
const f = fixture({
partitioner: partitionModel([{ chains: ["b"] }]),
problemGenerator: fauxEngine([factory]),
grader: echoGrader(1),
});
const pipeline = f.make();
let announces = 0;
const off = subscribe((e) => {
if (e.type === "activity-changed") announces++;
});
// Reads announce nothing.
practiceStatuses();
assert.equal(announces, 0);
const { done } = await pipeline.createProblemSet(["a", "b"], PARAMS);
await done;
// The lifecycle announced: partition row register + its finish,
// gen row register + the generating write + its finish.
assert.ok(announces >= 5, `register/phase/finish all announced (got ${announces})`);
const afterDone = announces;
practiceStatuses();
assert.equal(announces, afterDone, "reads stay silent");
off();
});
it("the learner's note threads to the partitioner + every generator and persists on the set (owner, 2026-09-29)", async () => {
const partSeen: string[] = [];
const genSeen: string[] = [];