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:
amosblomqvist
2026-09-23 15:37:34 +03:00
parent 1c3a5e121c
commit d1b46132b9
30 changed files with 181 additions and 595 deletions
+8 -66
View File
@@ -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
View File
@@ -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)),
});
-2
View File
@@ -57,8 +57,6 @@ function fakeStores(
readSources: () => [],
readPositions: () => ({}),
writePositions: () => {},
readEmbeddings: () => ({ model: "dead", dim: 0, vectors: {}, clusters: [], updatedAt: "" }),
writeEmbeddings: () => {},
readClusterLabels: () => [],
appendClusterLabel: () => {},
appendNamingRun: () => {},
+5 -20
View File
@@ -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;
+2
View File
@@ -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),
});
+1 -1
View File
@@ -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");
});
+76 -6
View File
@@ -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", () => {
+3 -3
View File
@@ -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);
});
+5 -5
View File
@@ -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", () => {
+3 -4
View File
@@ -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",
]);
});
});