Wave 7 close-out: the embedding ledger seam dies; notes birth via mapping lines
Part 1 — the ledger + remnants interlude (master-dispatched, owner ruling 2026-09-23: nothing configures the graph beyond chat models): - The embedding ledger surface is gone: readEmbeddings/writeEmbeddings ports, the EmbeddingsLedger contract, EMBEDDINGS_FILE (config, store impls, graphRevision source list), and graph-deletion's vector-pruning step. On-disk data files stay untouched (inert history). - The extraction embedder seam dies wholesale: ExtractionPipelineDeps drops the embedder dep (chat-handler + backfill/import-notes stubs go with it), the 'embedding' phase leaves ExtractionPhase, the usage capture/persist helpers die, and stages.embedding leaves ExtractionRunUsage (historical records keep their shape — reads are lenient). Extraction is two stages again, like the local-embedder era. - Consumer-less remnants: latestQuizOutcomes + QUIZ_EVENT_KINDS, clusterContentId, matchClusterLabelings + Community + ClusterLabeling, the readClusterLabels/appendClusterLabel port seams + store impls + LEDGER_FILES.clusterLabels. NamingRunUsage/readNamingRuns STAY (standing ruling — the cost pill renders historical naming spend); clusterHue + MIN_CLUSTER_SIZE stay (live). - The 'labeler' role dies: BACKGROUND_MODEL_ROLES, ROLE_SLOT, ModelRole, the catalog pin projections, and the UI roster strings. RolePins keys shrink with the union. The notes birth fallback (owner-approved 2026-09-23, surfaced by the interlude's live smoke: production notes imports create provenance-EMPTY nodes (importNotes passes messages: null — the file's anchor is the mapping line), so the provenance-only birth rule left every imported file unclustered and invisible to the prober's session list, against the locked file-birth ruling. A provenance-empty node now births under its FIRST mapping line's chatId (ledger order); only notes-born nodes ever hit the fallback (chat nodes always carry provenance; a re-encountered node's link-append re-opens the provenance rule). Deletion-safe: mapping lines of deleted chats are stripped alongside their provenance. Live-verified on port 3004: a fresh notes import clusters under its src_ id with the file path as the label, end to end with no embedder. 845/845 tests, typecheck/lint/build green. EOF )
This commit is contained in:
+8
-66
@@ -1,73 +1,16 @@
|
||||
/**
|
||||
* Community detection tests (vision §1, DECIDED 2026-09-13) — pure
|
||||
* structural Louvain over follows+supports, deterministic via seed.
|
||||
* Assertions are on SHAPE and MEMBER SETS, never community indices
|
||||
* (indices reshuffle; member sets are what labelings key to).
|
||||
* Cluster presentation-rule tests. Wave 7 killed the semantic detection
|
||||
* AND the LLM label matcher (birth-session clusters, session-name labels —
|
||||
* core/session-clusters.ts has its own suite); what remains pinned here
|
||||
* is the render threshold and the hull-color spread the UI leans on.
|
||||
*/
|
||||
import { strict as assert } from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import { matchClusterLabelings, MIN_CLUSTER_SIZE, type Community } from "../src/core/clusters.ts";
|
||||
import { MIN_CLUSTER_SIZE } from "../src/core/clusters.ts";
|
||||
import { clusterHue } from "../components/cluster-geometry.ts";
|
||||
import type { ClusterLabeling } from "../src/core/contracts.ts";
|
||||
|
||||
const ids = (n: number, prefix = "n") => [...Array(n).keys()].map((i) => `${prefix}${i}`);
|
||||
|
||||
/** A clique on the given ids. */
|
||||
const clique = (nodeIds: string[]) =>
|
||||
nodeIds.flatMap((a, i) => nodeIds.slice(i + 1).map((b) => ({ from: a, to: b })));
|
||||
|
||||
const labeling = (id: string, core: string[], createdAt = "2026-09-01T00:00:00.000Z"): ClusterLabeling => ({
|
||||
id,
|
||||
label: `label-${id}`,
|
||||
core,
|
||||
createdAt,
|
||||
});
|
||||
|
||||
describe("matchClusterLabelings", () => {
|
||||
test("claims a labeling while at least half its core survives in the community", () => {
|
||||
const community: Community = { members: ["a", "b", "c", "d", "e"] };
|
||||
const records = [labeling("clu_1", ["a", "b", "x", "y"])]; // 2 of 4 survive — exactly half
|
||||
const [claim] = matchClusterLabelings([community], records);
|
||||
assert.equal(claim?.id, "clu_1");
|
||||
// One fewer survivor: under half → relabel.
|
||||
const [dead] = matchClusterLabelings([{ members: ["a", "c", "d", "e", "f"] }], records);
|
||||
assert.equal(dead, null);
|
||||
});
|
||||
|
||||
test("single successor: a split core keeps its label in only one community", () => {
|
||||
const records = [labeling("clu_1", ["a", "b", "c", "d"])];
|
||||
const left: Community = { members: ["a", "b", "z"] }; // 2 of 4 — half
|
||||
const right: Community = { members: ["c", "d", "w"] }; // 2 of 4 — half
|
||||
// Tie: larger overlap wins; equal → both eligible, greedy takes first come.
|
||||
const claims = matchClusterLabelings([left, right], records);
|
||||
const claimed = claims.filter((c) => c !== null);
|
||||
assert.equal(claimed.length, 1);
|
||||
assert.equal(claimed[0]?.id, "clu_1");
|
||||
});
|
||||
|
||||
test("greedy: the best-overlapping community wins the record", () => {
|
||||
const records = [labeling("clu_1", ["a", "b", "c", "d"])];
|
||||
const weak: Community = { members: ["a", "b", "q", "r", "s"] }; // 2/4
|
||||
const strong: Community = { members: ["a", "b", "c", "x", "y"] }; // 3/4
|
||||
const claims = matchClusterLabelings([weak, strong], records);
|
||||
assert.equal(claims[0], null);
|
||||
assert.equal(claims[1]?.id, "clu_1");
|
||||
});
|
||||
|
||||
test("one record per community; newest record breaks ties", () => {
|
||||
const oldR = labeling("clu_old", ["a", "b"], "2026-09-01T00:00:00.000Z");
|
||||
const newR = labeling("clu_new", ["a", "b"], "2026-09-02T00:00:00.000Z");
|
||||
const community: Community = { members: ["a", "b", "c"] };
|
||||
const [claim] = matchClusterLabelings([community], [oldR, newR]);
|
||||
assert.equal(claim?.id, "clu_new");
|
||||
});
|
||||
|
||||
test("no labelings: every community unclaimed; empty communities array ok", () => {
|
||||
assert.deepEqual(matchClusterLabelings([{ members: ["a", "b"] }], []), [null]);
|
||||
assert.deepEqual(matchClusterLabelings([], [labeling("clu_1", ["a"])]), []);
|
||||
});
|
||||
|
||||
test("MIN_CLUSTER_SIZE is 3 (the render/naming threshold)", () => {
|
||||
describe("MIN_CLUSTER_SIZE", () => {
|
||||
test("is 3 (the render threshold — below this, no hull)", () => {
|
||||
assert.equal(MIN_CLUSTER_SIZE, 3);
|
||||
});
|
||||
});
|
||||
@@ -81,5 +24,4 @@ describe("clusterHue", () => {
|
||||
assert.ok(hues.size > 20, `expected spread, got ${hues.size} distinct hues for 40 ids`);
|
||||
for (const h of hues) assert.ok(h >= 0 && h < 360);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+4
-114
@@ -27,7 +27,6 @@ import type { UIMessage } from "ai";
|
||||
import type { PiEngine } from "../src/agent/models.ts";
|
||||
import type {
|
||||
AuditResult,
|
||||
ClusterLabeling,
|
||||
CoverageEntry,
|
||||
ExtractionRunUsage,
|
||||
GraphEdge,
|
||||
@@ -69,17 +68,9 @@ function memoryLedgers() {
|
||||
const checkpoints: SessionCheckpoint[] = [];
|
||||
const coverage: CoverageEntry[] = [];
|
||||
const sources = new Map<string, NotesSource>();
|
||||
const clusterLabels: ClusterLabeling[] = [];
|
||||
const namingRuns: NamingRunUsage[] = [];
|
||||
const namingRuns: NamingRunUsage[] = [];
|
||||
const positions = new Map<string, { x: number; y: number }>();
|
||||
const embeddings = {
|
||||
model: "",
|
||||
dim: 0,
|
||||
updatedAt: "",
|
||||
vectors: new Map<string, number[]>(),
|
||||
clusters: new Array<Array<string>>(),
|
||||
};
|
||||
return {
|
||||
return {
|
||||
appendNode: (n: GraphNode) => void nodes.push(n),
|
||||
readNodes: () => nodes.map((n) => ({ ...n })),
|
||||
appendEdge: (e: GraphEdge) => void edges.push(e),
|
||||
@@ -108,10 +99,6 @@ function memoryLedgers() {
|
||||
}
|
||||
},
|
||||
// Cluster labelings — mirrors the real store: append dedupes by id.
|
||||
appendClusterLabel: (l: ClusterLabeling) => {
|
||||
if (!clusterLabels.some((x) => x.id === l.id)) clusterLabels.push(l);
|
||||
},
|
||||
readClusterLabels: () => clusterLabels.map((l) => ({ ...l, core: [...l.core] })),
|
||||
// Cluster-naming run usage — mirrors the real store: dedupe per startedAt.
|
||||
appendNamingUsage: (entry: NamingRunUsage) => {
|
||||
if (!namingRuns.some((r) => r.startedAt === entry.startedAt)) namingRuns.push(entry);
|
||||
@@ -123,16 +110,6 @@ function memoryLedgers() {
|
||||
positions.clear();
|
||||
for (const [id, p] of Object.entries(next)) positions.set(id, { x: p.x, y: p.y });
|
||||
},
|
||||
// The embedding ledger — the embedder writes it after a merge.
|
||||
readEmbeddings: () => ({ model: embeddings.model, dim: embeddings.dim, vectors: Object.fromEntries([...embeddings.vectors].map(([id, v]) => [id, [...v]])), clusters: [...embeddings.clusters.map((c) => ({ members: [...c] }))], updatedAt: embeddings.updatedAt }),
|
||||
writeEmbeddings: (next) => {
|
||||
embeddings.model = next.model;
|
||||
embeddings.dim = next.dim;
|
||||
embeddings.updatedAt = next.updatedAt;
|
||||
embeddings.vectors.clear();
|
||||
for (const [id, v] of Object.entries(next.vectors)) embeddings.vectors.set(id, [...v]);
|
||||
embeddings.clusters = next.clusters.map((c) => [...c.members]);
|
||||
},
|
||||
// The destructive seam (wave 6 session deletion) — mirrors the real
|
||||
// store: replace the provided ledgers' full contents.
|
||||
rewriteLedgers: (next) => {
|
||||
@@ -360,7 +337,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: submitModel("submit_review", verdict),
|
||||
});
|
||||
@@ -414,7 +390,7 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
// No direct engines AND no engineFor — the chain came up empty.
|
||||
const pipeline = createExtractionPipeline({ chatStore, graphStore: ledgers, embedder: fakeEmbedder });
|
||||
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
|
||||
@@ -438,7 +414,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: submitModel("submit_review", verdict),
|
||||
});
|
||||
@@ -448,7 +423,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
// so the post-merge naming garnish is gone — no model spend, no
|
||||
// naming-run records, no label writes.
|
||||
assert.equal(ledgers.readNamingRuns().length, 0, "no naming runs minted");
|
||||
assert.equal(ledgers.readClusterLabels().length, 0, "no label records appended");
|
||||
});
|
||||
|
||||
|
||||
@@ -466,7 +440,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
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).
|
||||
@@ -498,7 +471,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: submitModel("submit_review", verdict),
|
||||
// If the direct values did NOT win, this throw would surface.
|
||||
@@ -517,7 +489,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: silentModel(), // never submits the review
|
||||
});
|
||||
@@ -528,7 +499,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const failingExtractor = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: silentModel(),
|
||||
auditor: submitModel("submit_review", { ...FINDINGS, dropped: [] }),
|
||||
});
|
||||
@@ -559,7 +529,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
// 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())),
|
||||
@@ -570,48 +539,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
assert.equal(extractionStatuses().length, 0, "the registry clears when the run ends");
|
||||
});
|
||||
|
||||
it("labels the post-batch embedding tail as its own phase — not 'merging' (2026-09-19 accuracy fix)", async () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
// Gate the embedder so we can observe the registry WHILE the tail
|
||||
// runs — mergeFindings has already returned, every batch is merged.
|
||||
let releaseEmbed!: () => void;
|
||||
const embedGate = new Promise<void>((resolve) => {
|
||||
releaseEmbed = resolve;
|
||||
});
|
||||
let observed: string | undefined;
|
||||
const gatedEmbedder = async (store: GraphStore) => {
|
||||
await embedGate;
|
||||
observed = extractionStatuses().find((r) => r.chatId === chatId)?.phase;
|
||||
await fakeEmbedder(store);
|
||||
};
|
||||
const verdict: AuditResult = { propositions: FINDINGS.propositions, edges: [], attribution: { quiz: {} }, dropped: [] };
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: gatedEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: submitModel("submit_review", verdict),
|
||||
});
|
||||
const run = pipeline.runExtraction(chatId);
|
||||
// Wait for the tail: the phase must flip to "embedding" while the
|
||||
// embedder hangs — the incident this fixes is minutes of "merging"
|
||||
// AFTER the last merge, so the flip itself is the assertion.
|
||||
const t0 = Date.now();
|
||||
while (extractionStatuses().find((r) => r.chatId === chatId)?.phase !== "embedding") {
|
||||
if (Date.now() - t0 > 2000) {
|
||||
releaseEmbed();
|
||||
await run;
|
||||
assert.fail("the tail never flipped to embedding (stuck at 'merging'?)");
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
}
|
||||
releaseEmbed();
|
||||
await run;
|
||||
assert.equal(observed, "embedding", "still embedding through the tail, never back to 'merging'");
|
||||
assert.equal(extractionStatuses().length, 0, "clears when done");
|
||||
});
|
||||
|
||||
it("captures per-stage usage live, persists the run's usage into the chat record", async () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
@@ -634,7 +561,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
// 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),
|
||||
@@ -676,7 +602,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: hangingModel(), // hangs until aborted — no step finishes
|
||||
auditor: submitModel("submit_review", { propositions: [], edges: [], attribution: { quiz: {} }, dropped: [] }),
|
||||
});
|
||||
@@ -727,7 +652,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: gatedExtractor,
|
||||
auditor: silentModel(),
|
||||
});
|
||||
@@ -761,7 +685,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: gatedExtractor,
|
||||
auditor: silentModel(),
|
||||
});
|
||||
@@ -771,7 +694,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const observer = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: memoryLedgers(),
|
||||
embedder: fakeEmbedder,
|
||||
extractor: silentModel(),
|
||||
auditor: silentModel(),
|
||||
});
|
||||
@@ -798,7 +720,6 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: silentModel(),
|
||||
auditor: silentModel(),
|
||||
});
|
||||
@@ -838,28 +759,6 @@ const NOTES_VERDICT = {
|
||||
dropped: [],
|
||||
} as const;
|
||||
|
||||
/** Test seam for the embedder: semantic clustering died with wave 7
|
||||
* (birth-session clusters + session-name labels), so the production
|
||||
* embedder is a no-op stub pending Platform's ledger-port removal —
|
||||
* this fake mirrors it: one identical vector per node persisted to the
|
||||
* (still-live) embeddings ledger port, no clusters computed. */
|
||||
const fakeEmbedder = async (store: GraphStore) => {
|
||||
const seen = new Set<string>();
|
||||
const ids: string[] = [];
|
||||
for (const n of store.readNodes()) {
|
||||
if (seen.has(n.id)) continue;
|
||||
seen.add(n.id);
|
||||
ids.push(n.id);
|
||||
}
|
||||
store.writeEmbeddings({
|
||||
model: "test/embedder",
|
||||
dim: 3,
|
||||
vectors: Object.fromEntries(ids.map((id) => [id, [1, 0, 0]])),
|
||||
clusters: [],
|
||||
updatedAt: "2026-09-13T00:00:00.000Z",
|
||||
});
|
||||
};
|
||||
|
||||
describe("the notes front door (importNotes — format-agnostic)", () => {
|
||||
it("imports raw notes end to end: source record, file-scoped provenance, no quiz events", async () => {
|
||||
const ledgers = memoryLedgers();
|
||||
@@ -867,7 +766,6 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", NOTES_FINDINGS),
|
||||
// During the auditor's call the registry must show the notes run.
|
||||
auditor: submitModel("submit_review", NOTES_VERDICT, () => {
|
||||
@@ -915,8 +813,7 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
|
||||
createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", NOTES_FINDINGS),
|
||||
extractor: submitModel("submit_findings", NOTES_FINDINGS),
|
||||
auditor: submitModel("submit_review", NOTES_VERDICT),
|
||||
});
|
||||
const first = await mk().importNotes({ filePath: "/vault/learn vim.md", text: NOTES_TEXT });
|
||||
@@ -935,7 +832,6 @@ describe("the notes front door (importNotes — format-agnostic)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: hangingModel(),
|
||||
auditor: submitModel("submit_review", NOTES_VERDICT),
|
||||
});
|
||||
@@ -1017,7 +913,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: fauxEngine([findingsStep((v) => views.push(v)), findingsStep((v) => views.push(v))]),
|
||||
auditor: fauxEngine([passThroughAuditStep(), passThroughAuditStep()]),
|
||||
});
|
||||
@@ -1059,7 +954,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
// Batch 1 submits and merges; batch 2's extractor call HANGS
|
||||
// until the abort fires — the dock's abort button, mid-run.
|
||||
extractor: fauxEngine([
|
||||
@@ -1103,7 +997,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
|
||||
const finisher = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: fauxEngine([findingsStep((v) => reRunViews.push(v))]),
|
||||
auditor: fauxEngine([passThroughAuditStep()]),
|
||||
});
|
||||
@@ -1125,7 +1018,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: fauxEngine([findingsStep()]),
|
||||
auditor: fauxEngine([passThroughAuditStep()]),
|
||||
});
|
||||
@@ -1157,7 +1049,6 @@ describe("the chunked pipeline (extraction coverage)", () => {
|
||||
const reOpener = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: fauxEngine([findingsStep((v) => reRunViews.push(v))]),
|
||||
auditor: fauxEngine([passThroughAuditStep()]),
|
||||
});
|
||||
@@ -1180,7 +1071,6 @@ describe("the session-deletion guard (chat deleted mid-run)", () => {
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
embedder: fakeEmbedder,
|
||||
extractor: submitModel("submit_findings", FINDINGS),
|
||||
auditor: submitModel("submit_review", { propositions: FINDINGS.propositions, edges: [], attribution: { quiz: {} }, dropped: [] }, () => chatStore.deleteChat(chatId)),
|
||||
});
|
||||
|
||||
@@ -57,8 +57,6 @@ function fakeStores(
|
||||
readSources: () => [],
|
||||
readPositions: () => ({}),
|
||||
writePositions: () => {},
|
||||
readEmbeddings: () => ({ model: "dead", dim: 0, vectors: {}, clusters: [], updatedAt: "" }),
|
||||
writeEmbeddings: () => {},
|
||||
readClusterLabels: () => [],
|
||||
appendClusterLabel: () => {},
|
||||
appendNamingRun: () => {},
|
||||
|
||||
+5
-20
@@ -23,7 +23,7 @@ import type { QuestionAnswerData } from "../src/core/question-core.ts";
|
||||
import { getGraph, graphSnapshot, mergeFindings, type GraphDeps } from "../src/core/graph.ts";
|
||||
import { graphStore } from "../src/store/graph.ts";
|
||||
import { chatStore } from "../src/store/chats.ts";
|
||||
import { LEDGER_FILES, PATHS, SOURCES_FILE, EMBEDDINGS_FILE } from "../config.ts";
|
||||
import { LEDGER_FILES, PATHS, SOURCES_FILE } from "../config.ts";
|
||||
|
||||
/** The wired fs adapters, exactly what src/server/stores.ts passes in production. */
|
||||
const graphDeps = { chatStore, graphStore } as const satisfies GraphDeps;
|
||||
@@ -38,9 +38,7 @@ function sessionSource(chatId: string): { messages: UIMessage[]; foldedAt: strin
|
||||
|
||||
// ─── ledger isolation (backup-and-clear of data/graph/*.jsonl) ──────────────
|
||||
|
||||
// EMBEDDINGS_FILE too: the semantic-cluster source (read by graphSnapshot)
|
||||
// — isolated like the rest so this suite never touches the real vectors.
|
||||
const LEDGER_PATHS = [...Object.values(LEDGER_FILES), SOURCES_FILE, EMBEDDINGS_FILE];
|
||||
const LEDGER_PATHS = [...Object.values(LEDGER_FILES), SOURCES_FILE];
|
||||
const backups: Array<{ file: string; bak: string }> = [];
|
||||
const createdChats: string[] = [];
|
||||
|
||||
@@ -681,21 +679,14 @@ describe("session deletion (deleteChatTraces + deleteChatSession, wave 6)", () =
|
||||
mergeFindings(dB.id, auditB, graphDeps, sessionSource(dB.id));
|
||||
const zId = graphStore.readMappings().find((m) => m.chatId === dB.id && m.sourceId === "q2")!.graphNodeId;
|
||||
|
||||
// Coverage rows (both chats) + positions + embeddings garnish — the
|
||||
// deletion prunes all three ledgers.
|
||||
// Coverage rows (both chats) + positions garnish — the deletion
|
||||
// prunes both ledgers.
|
||||
for (const chat of [dA, dB]) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
graphStore.appendCoverage({ chatId: chat.id, fp: `${chat.id}-${i}`, tokens: 10, foldedAt: "2026-09-23T00:02:00.000Z" });
|
||||
}
|
||||
}
|
||||
graphStore.writePositions({ ...graphStore.readPositions(), [xId]: { x: 1, y: 1 }, [yId]: { x: 2, y: 2 }, [zId]: { x: 3, y: 3 } });
|
||||
const emb = graphStore.readEmbeddings();
|
||||
graphStore.writeEmbeddings({
|
||||
...emb,
|
||||
vectors: { ...emb.vectors, [xId]: [1, 0], [yId]: [0, 1], [zId]: [1, 1] },
|
||||
clusters: [...emb.clusters, { members: [xId, yId, zId] }],
|
||||
updatedAt: "2026-09-23T00:03:00.000Z",
|
||||
});
|
||||
return { dA, dB, xId, yId, zId };
|
||||
}
|
||||
|
||||
@@ -738,14 +729,10 @@ describe("session deletion (deleteChatTraces + deleteChatSession, wave 6)", () =
|
||||
// Mappings: dA's audit trail died; dB's stays.
|
||||
const mappings = graphStore.readMappings().filter((m) => [dA.id, dB.id].includes(m.chatId));
|
||||
assert.deepEqual(mappings, [{ chatId: dB.id, sourceId: "q1", graphNodeId: xId }, { chatId: dB.id, sourceId: "q2", graphNodeId: zId }]);
|
||||
// Positions/embeddings: the orphan pruned everywhere.
|
||||
// Positions: the orphan pruned.
|
||||
const positions = graphStore.readPositions();
|
||||
assert.equal(positions[yId], undefined);
|
||||
assert.deepEqual(positions[xId], { x: 1, y: 1 });
|
||||
const emb = graphStore.readEmbeddings();
|
||||
assert.equal(emb.vectors[yId], undefined);
|
||||
assert.ok(emb.vectors[xId] && emb.vectors[zId]);
|
||||
assert.deepEqual(emb.clusters.at(-1), { members: [xId, zId] }, "dead members pruned from the hull");
|
||||
// The snapshot resolves X to dB only — the deleted chat left no ghost.
|
||||
const snapshot = graphSnapshot(graphDeps);
|
||||
assert.deepEqual(snapshot.nodeSessions[xId], [dB.id]);
|
||||
@@ -768,7 +755,6 @@ describe("session deletion (deleteChatTraces + deleteChatSession, wave 6)", () =
|
||||
events: graphStore.readEvents(),
|
||||
mappings: graphStore.readMappings(),
|
||||
positions: graphStore.readPositions(),
|
||||
embeddings: graphStore.readEmbeddings(),
|
||||
};
|
||||
const stats = deleteChatTraces(graphStore, dA.id, "preserve-graph");
|
||||
assert.deepEqual(stats, {
|
||||
@@ -784,7 +770,6 @@ describe("session deletion (deleteChatTraces + deleteChatSession, wave 6)", () =
|
||||
assert.deepEqual(graphStore.readEvents(), before.events);
|
||||
assert.deepEqual(graphStore.readMappings(), before.mappings);
|
||||
assert.deepEqual(graphStore.readPositions(), before.positions);
|
||||
assert.deepEqual(graphStore.readEmbeddings(), before.embeddings);
|
||||
assert.equal(graphStore.readCoverage().some((row) => row.chatId === dA.id), false);
|
||||
assert.equal(graphStore.readCoverage().some((row) => row.chatId === dB.id), true);
|
||||
void xId; void yId; void zId;
|
||||
|
||||
@@ -108,6 +108,7 @@ describe("settleGraphLayout", () => {
|
||||
const store = {
|
||||
readNodes: () => lines.map((l) => ({ ...l })),
|
||||
readEdges: () => [edge("a", "b")],
|
||||
readMappings: () => [],
|
||||
readPositions: () => ({} as Record<string, Position>),
|
||||
writePositions: (p: Record<string, Position>) => void written.push(p),
|
||||
};
|
||||
@@ -121,6 +122,7 @@ describe("settleGraphLayout", () => {
|
||||
settleGraphLayout({
|
||||
readNodes: () => [],
|
||||
readEdges: () => [],
|
||||
readMappings: () => [],
|
||||
readPositions: () => ({} as Record<string, Position>),
|
||||
writePositions: (p) => void written.push(p),
|
||||
});
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("the background roles' resolved pins (rolePins)", () => {
|
||||
};
|
||||
const pins = rolePinsFor(resolveSettings(record).agent.models);
|
||||
assert.deepEqual(pins.researcher, { model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" }, thinkingLevel: "high" });
|
||||
assert.deepEqual(pins.labeler, { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } }); // level unset
|
||||
assert.deepEqual(pins.auditor, { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } }); // level unset
|
||||
assert.equal(pins.visualMaker, null, "vision tier untouched");
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ function fakeStores(
|
||||
events?: LedgerEvent[];
|
||||
chats?: Array<Pick<ChatSummary, "id" | "title" | "createdAt" | "updatedAt">>;
|
||||
sources?: Array<Pick<NotesSource, "id" | "filePath" | "importedAt">>;
|
||||
mappings?: Array<{ chatId: string; sourceId: string; graphNodeId: string }>;
|
||||
} = {},
|
||||
): GraphDeps {
|
||||
const edges: GraphEdge[] = [];
|
||||
@@ -46,18 +47,13 @@ function fakeStores(
|
||||
readNodes: () => nodes,
|
||||
readEdges: () => edges,
|
||||
readEvents: () => over.events ?? [],
|
||||
readMappings: () => [],
|
||||
readMappings: () => over.mappings ?? [],
|
||||
readCheckpoints: () => [],
|
||||
readCoverage: () => [],
|
||||
readSources: () =>
|
||||
(over.sources ?? []).map((s) => ({ ...s, extractions: [] }) as unknown as NotesSource),
|
||||
readPositions: () => ({}),
|
||||
writePositions: () => {},
|
||||
readEmbeddings: () => ({ model: "dead", dim: 0, vectors: {}, clusters: [], updatedAt: "" }),
|
||||
writeEmbeddings: () => {},
|
||||
readClusterLabels: () => [],
|
||||
appendClusterLabel: () => {},
|
||||
appendNamingRun: () => {},
|
||||
readNamingRuns: () => [],
|
||||
} as unknown as GraphStore;
|
||||
const chatStore: ChatStore = {
|
||||
@@ -110,6 +106,80 @@ describe("birthSessionPartition", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── the notes mapping fallback (owner-approved 2026-09-23) ─────────────────
|
||||
|
||||
describe("the notes mapping-line fallback", () => {
|
||||
// The PRODUCTION notes shape: importNotes passes messages: null, so
|
||||
// nodes are born with EMPTY provenance — the file's anchor is the
|
||||
// mapping line (interlude live-smoke finding: the provenance-only rule
|
||||
// left every imported file unclustered and invisible to the prober).
|
||||
it("empty-provenance nodes birth under their first mapping line's session", () => {
|
||||
const nodes = [
|
||||
node("a", "Alpha", []),
|
||||
node("b", "Beta", []),
|
||||
node("c", "Gamma", []),
|
||||
node("d", "Delta", [pt("chat1")]), // provenance wins — mapping ignored
|
||||
];
|
||||
const mappings = [
|
||||
{ chatId: "src_notes", sourceId: "p1", graphNodeId: "a" },
|
||||
{ chatId: "src_notes", sourceId: "p2", graphNodeId: "b" },
|
||||
{ chatId: "src_notes", sourceId: "p3", graphNodeId: "c" },
|
||||
// A chat mapping for a node that ALSO has provenance: never consulted.
|
||||
{ chatId: "chat2", sourceId: "p4", graphNodeId: "d" },
|
||||
// A mapping for a node that does not exist: ignored.
|
||||
{ chatId: "src_notes", sourceId: "p5", graphNodeId: "ghost" },
|
||||
];
|
||||
const clusters = birthSessionPartition(nodes, mappings);
|
||||
assert.deepEqual(clusters, [
|
||||
{ id: "src_notes", members: ["a", "b", "c"] },
|
||||
{ id: "chat1", members: ["d"] },
|
||||
], "size-desc, then id asc — src_notes first");
|
||||
// No mapping and no provenance: unclustered.
|
||||
assert.deepEqual(birthSessionPartition([node("x", "X", [])], mappings), []);
|
||||
});
|
||||
|
||||
it("graphSessionIndex lists imported files as kind:file with birth counts (production shape)", () => {
|
||||
const nodes = [node("a", "Alpha", []), node("b", "Beta", []), node("c", "Gamma", [])];
|
||||
const deps = fakeStores(nodes, {
|
||||
mappings: [
|
||||
{ chatId: "src_abc", sourceId: "p1", graphNodeId: "a" },
|
||||
{ chatId: "src_abc", sourceId: "p2", graphNodeId: "b" },
|
||||
{ chatId: "src_abc", sourceId: "p3", graphNodeId: "c" },
|
||||
],
|
||||
sources: [{ id: "src_abc", filePath: "/vault/learn vim.md", importedAt: "2026-09-22T00:00:00.000Z" }],
|
||||
});
|
||||
const index = graphSessionIndex(deps);
|
||||
assert.deepEqual(index, [
|
||||
{
|
||||
id: "src_abc",
|
||||
name: "/vault/learn vim.md",
|
||||
kind: "file",
|
||||
createdAt: "2026-09-22T00:00:00.000Z",
|
||||
updatedAt: "2026-09-22T00:00:00.000Z",
|
||||
propositions: 3,
|
||||
},
|
||||
], "the file births under its src id; the childless chat has no entry (birth sessions only)");
|
||||
});
|
||||
|
||||
it("graphSessionPropositions lists the file's births under the src id", () => {
|
||||
const events: LedgerEvent[] = [
|
||||
{ nodeId: "a", kind: "observation", chatId: "src_abc", at: "2026-09-22T00:00:00.000Z", note: "solid", ref: "obs:p1" },
|
||||
];
|
||||
const deps = fakeStores([node("a", "Alpha", []), node("b", "Beta", [])], {
|
||||
events,
|
||||
mappings: [
|
||||
{ chatId: "src_abc", sourceId: "p1", graphNodeId: "a" },
|
||||
{ chatId: "src_abc", sourceId: "p2", graphNodeId: "b" },
|
||||
],
|
||||
sources: [{ id: "src_abc", filePath: "/vault/learn vim.md", importedAt: "2026-09-22T00:00:00.000Z" }],
|
||||
});
|
||||
assert.deepEqual(graphSessionPropositions(deps, "src_abc"), [
|
||||
{ nodeId: "a", name: "Alpha", understanding: "solid", evaluatedAt: "2026-09-22T00:00:00.000Z" },
|
||||
{ nodeId: "b", name: "Beta", understanding: null, evaluatedAt: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── the snapshot join ──────────────────────────────────────────────────────
|
||||
|
||||
describe("graphSnapshot birth-session clusters", () => {
|
||||
|
||||
@@ -39,17 +39,17 @@ describe("parseAgentSettingsBody", () => {
|
||||
const r = parseAgentSettingsBody({
|
||||
models: {
|
||||
researcher: { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } },
|
||||
labeler: { thinkingLevel: "high" },
|
||||
auditor: { thinkingLevel: "high" },
|
||||
},
|
||||
});
|
||||
assert.ok(r.ok);
|
||||
assert.deepEqual(r.settings.models, {
|
||||
researcher: { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } },
|
||||
labeler: { thinkingLevel: "high" },
|
||||
auditor: { thinkingLevel: "high" },
|
||||
});
|
||||
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, labeler\)/);
|
||||
if (!bad.ok) assert.match(bad.error, /unknown role "teacher" \(supported: researcher, demoBuilder, visualMaker, extractor, auditor\)/);
|
||||
const malformed = parseAgentSettingsBody({ models: { researcher: { model: { provider: 1, id: "x" } } } });
|
||||
assert.equal(malformed.ok, false);
|
||||
});
|
||||
|
||||
@@ -97,22 +97,22 @@ describe("resolveSettings", () => {
|
||||
it("resolveModelSpec is field-independent per override aspect", () => {
|
||||
const slots = { cheap: { model: { provider: "zai", id: "slot-model" }, thinkingLevel: "high" as const } };
|
||||
assert.deepEqual(
|
||||
resolveModelSpec("labeler", { model: { provider: "other", id: "x" } }, slots),
|
||||
resolveModelSpec("auditor", { model: { provider: "other", id: "x" } }, slots),
|
||||
{ provider: "other", id: "x", thinkingLevel: "high" }, // override model + slot level
|
||||
);
|
||||
assert.deepEqual(resolveModelSpec("labeler", { thinkingLevel: "max" }, slots), {
|
||||
assert.deepEqual(resolveModelSpec("auditor", { thinkingLevel: "max" }, slots), {
|
||||
provider: "zai",
|
||||
id: "slot-model",
|
||||
thinkingLevel: "max", // override level + slot model
|
||||
});
|
||||
assert.equal(resolveModelSpec("labeler", undefined, {}), undefined);
|
||||
assert.equal(resolveModelSpec("auditor", undefined, {}), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the slot tier (BYOK, owner 2026-09-22)", () => {
|
||||
it("slot alone re-tiers every role of its tier (no role overrides present)", () => {
|
||||
const eff = resolveSettings({ version: 1, models: { cheap: { model: { provider: "zai", id: "glm-5.3-flash" } } } });
|
||||
for (const role of ["researcher", "demoBuilder", "extractor", "auditor", "labeler"] as const) {
|
||||
for (const role of ["researcher", "demoBuilder", "extractor", "auditor"] as const) {
|
||||
assert.deepEqual(
|
||||
eff.agent.models[role],
|
||||
{ provider: "zai", id: "glm-5.3-flash", thinkingLevel: undefined }, // level unset → default order
|
||||
@@ -134,7 +134,7 @@ describe("the slot tier (BYOK, owner 2026-09-22)", () => {
|
||||
id: "r-override",
|
||||
thinkingLevel: "high", // slot level rides the override's model
|
||||
});
|
||||
assert.deepEqual(eff.agent.models.labeler, { provider: "zai", id: "r-slot", thinkingLevel: "high" });
|
||||
assert.deepEqual(eff.agent.models.auditor, { provider: "zai", id: "r-slot", thinkingLevel: "high" });
|
||||
});
|
||||
|
||||
it("vision slot covers visualMaker AND pdfReader; level-only slots resolve as NULL", () => {
|
||||
|
||||
@@ -145,7 +145,7 @@ describe("sanitizeUserSettings", () => {
|
||||
models: {
|
||||
teacher: { thinkingLevel: "low" }, // NOT a background role — drops
|
||||
researcher: { model: { provider: "openrouter", id: "x/y" } },
|
||||
labeler: { model: { provider: 3, id: "x/y" }, thinkingLevel: "high" }, // bad model, keeps thinking
|
||||
extractor: { model: { provider: 3, id: "x/y" }, thinkingLevel: "high" }, // bad model, keeps thinking
|
||||
auditor: { thinkingLevel: "banana" }, // invalid level → empty override drops
|
||||
},
|
||||
},
|
||||
@@ -154,7 +154,7 @@ describe("sanitizeUserSettings", () => {
|
||||
const models = out.agent?.models ?? {};
|
||||
assert.equal("teacher" in models, false);
|
||||
assert.deepEqual(models.researcher, { model: { provider: "openrouter", id: "x/y" } });
|
||||
assert.deepEqual(models.labeler, { thinkingLevel: "high" });
|
||||
assert.deepEqual(models.extractor, { thinkingLevel: "high" });
|
||||
assert.equal("auditor" in models, false);
|
||||
});
|
||||
|
||||
@@ -186,14 +186,13 @@ describe("sanitizeUserSettings", () => {
|
||||
assert.deepEqual(out.agent?.tools, { quiz: false, demo: true });
|
||||
});
|
||||
|
||||
it("BACKGROUND_MODEL_ROLES matches the six background roles (config sync pinned in server tests)", () => {
|
||||
it("BACKGROUND_MODEL_ROLES matches the five background roles (the labeler died with the wave-7 namer kill)", () => {
|
||||
assert.deepEqual([...BACKGROUND_MODEL_ROLES], [
|
||||
"researcher",
|
||||
"demoBuilder",
|
||||
"visualMaker",
|
||||
"extractor",
|
||||
"auditor",
|
||||
"labeler",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user