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);
});
});
});