feat(linkedin-studio): SB-S3c — cross-silo id-threading + post→analytics assembler [skip-docs]

Hub-side design: the published record now carries the specifics/trends ids
it was built from (additive, omit-empty → byte-backward-compatible), and a
new pure assembler (scripts/brain/src/assemble.ts + `brain assemble`) joins
post↔analytics by normalized title-prefix + date with honest confidence
tiers (high/low/none). Answers the arc's north-star query: which raw
material actually performs? (specific → post → measured analytics).

All four tributaries untouched (analytics READ-only via inlined raw-JSON,
no package import); profile.md grammar untouched (the fact→post link stays
OUT — C-1). The repeatable --specific/--trend ingest flags collect via a
new collectRepeated helper, leaving parseFlags untouched.

TDD: 19 new brain tests (ingest 4 + publish 3 + assemble 8 + cli 4), all
SC1–SC12. brain 113/113, gate 95/0/0, BRAIN_TESTS_FLOOR 94→113,
ASSERT_BASELINE_FLOOR unchanged at 80. Light-Voyage hardened
(brief-review 5 FIX · plan-critic 1 BLOCK+4 MAJOR+4 MINOR · scope-guardian ALIGNED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 20:47:34 +02:00
commit edd3e15ef7
12 changed files with 515 additions and 14 deletions

View file

@ -0,0 +1,95 @@
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { mintContentId } from "../src/id.js";
import type { PublishedRecord } from "../src/ingest.js";
import { assemblePostGraph, type AnalyticsRowInput } from "../src/assemble.js";
const DATE = "2026-05-26";
function rec(body: string, over: Partial<PublishedRecord> = {}): PublishedRecord {
return {
id: mintContentId(body),
provenance: "published",
published_date: DATE,
captured_at: "2026-06-23",
source: "manual",
specifics: [],
trends: [],
body,
...over,
};
}
function row(title: string, over: Partial<AnalyticsRowInput> = {}): AnalyticsRowInput {
return { title, publishedDate: DATE, metrics: { engagementRate: 4.2 }, ...over };
}
// A body whose normalized opener is ≥ PREFIX_FLOOR (24) chars.
const BODY = "Jeg lærte noe viktig om dømmekraft i dag.\n\nDel 1 av serien.";
const FULL_TITLE = "Jeg lærte noe viktig om dømmekraft i dag.";
describe("SB-S3c assembler — assemblePostGraph (SC6/SC7/SC8)", () => {
// SC6 — high confidence: body begins with the row title (≥floor) + same date.
test("SC6: prefix-match + same date → high, whole row attached, specifics/trends surfaced", () => {
const r = rec(BODY, { specifics: ["aaaaaaaaaaaa"], trends: ["bbbbbbbbbbbb"] });
const graph = assemblePostGraph({ records: [r], analytics: [row(FULL_TITLE)] });
assert.equal(graph.length, 1);
assert.equal(graph[0].contentId, r.id);
assert.equal(graph[0].match.confidence, "high");
assert.deepEqual(graph[0].match.row, row(FULL_TITLE)); // the whole row reference (FIX 4)
assert.deepEqual(graph[0].specifics, ["aaaaaaaaaaaa"]);
assert.deepEqual(graph[0].trends, ["bbbbbbbbbbbb"]);
});
// SC7 — low / none / below-floor / ellipsis near-miss.
test("SC7a: no prefix match → none", () => {
const graph = assemblePostGraph({
records: [rec(BODY)],
analytics: [row("Completely unrelated opening sentence here")],
});
assert.equal(graph[0].match.confidence, "none");
assert.equal(graph[0].match.row, undefined);
});
test("SC7b: a too-short (< floor) title that is a literal prefix → none (floor guards false high)", () => {
const graph = assemblePostGraph({ records: [rec(BODY)], analytics: [row("Jeg lærte")] });
assert.equal(graph[0].match.confidence, "none");
});
test("SC7c: a LinkedIn-truncated '…' title + same date → high (ellipsis stripped)", () => {
const graph = assemblePostGraph({
records: [rec(BODY)],
analytics: [row("Jeg lærte noe viktig om dømmekraft i…")],
});
assert.equal(graph[0].match.confidence, "high");
});
test("SC7d: prefix match but a different date → low (surfaced, not hidden)", () => {
const graph = assemblePostGraph({
records: [rec(BODY)],
analytics: [row(FULL_TITLE, { publishedDate: "2026-05-20" })],
});
assert.equal(graph[0].match.confidence, "low");
assert.ok(graph[0].match.row, "row still attached on a low match");
});
// SC8 — pure + total.
test("SC8: empty records → empty graph", () => {
assert.deepEqual(assemblePostGraph({ records: [], analytics: [row(FULL_TITLE)] }), []);
});
test("SC8: empty analytics → every post present with match none, no throw", () => {
const graph = assemblePostGraph({ records: [rec(BODY), rec("another post body here")], analytics: [] });
assert.equal(graph.length, 2);
assert.ok(graph.every((g) => g.match.confidence === "none"));
});
test("SC8: best-of prefers high over low and is deterministic", () => {
const r = rec(BODY);
const high = row(FULL_TITLE); // same date
const low = row(FULL_TITLE, { publishedDate: "2026-01-01" }); // different date
const graph = assemblePostGraph({ records: [r], analytics: [low, high] });
assert.equal(graph[0].match.confidence, "high", "high beats low regardless of input order");
});
});

View file

@ -1,7 +1,7 @@
import { describe, test, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs";
import { mkdtempSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
@ -75,4 +75,58 @@ describe("brain CLI dispatch (SB-S1)", () => {
const { code } = runCli(root, ["bogus"]);
assert.equal(code, 2);
});
// SC5 — repeatable producer flags collect into arrays.
test("SC5: `ingest --specific a --specific c --trend b` tags the record [a,c]/[b]", () => {
const f = join(root, "post.md");
writeFileSync(f, "A tagged published post body.", "utf8");
const { stdout, code } = runCli(root, [
"ingest", "--file", f,
"--specific", "aaaaaaaaaaaa", "--specific", "cccccccccccc", "--trend", "bbbbbbbbbbbb",
]);
assert.equal(code, 0);
const idMatch = stdout.match(/published\/([0-9a-f]{12})\.md/);
assert.ok(idMatch, "wrote a record");
const recText = readFileSync(join(root, "ingest", "published", `${idMatch![1]}.md`), "utf8");
assert.match(recText, /specifics: aaaaaaaaaaaa,cccccccccccc/);
assert.match(recText, /trends: bbbbbbbbbbbb/);
});
// SC12 — single-value flags unregressed by the repeatable-flag change.
test("SC12: single-value `--source` + boolean `--scan-inbox` still parse as today", () => {
const f = join(root, "p.md");
writeFileSync(f, "single-flag body", "utf8");
runCli(root, ["ingest", "--file", f, "--source", "connector-x"]);
const recDir = join(root, "ingest", "published");
const recFile = readFileSync(join(recDir, readdirSync(recDir)[0]), "utf8");
assert.match(recFile, /source: connector-x/);
const { code } = runCli(root, ["ingest", "--scan-inbox"]);
assert.equal(code, 0);
});
// SC9 — read-only `assemble` prints the join and writes nothing.
test("SC9: `assemble` joins post↔analytics and writes nothing", () => {
const f = join(root, "post.md");
const body = "Jeg lærte noe viktig om dømmekraft i dag. Del 1.";
writeFileSync(f, body, "utf8");
runCli(root, ["ingest", "--file", f, "--date", "2026-05-26", "--specific", "aaaaaaaaaaaa"]);
// Seed an analytics batch JSON (raw shape; assemble inlines the read).
const postsDir = join(root, "analytics", "posts");
mkdirSync(postsDir, { recursive: true });
writeFileSync(
join(postsDir, "2026-05-26-batch.json"),
JSON.stringify({ posts: [{ title: "Jeg lærte noe viktig om dømmekraft i dag.", publishedDate: "2026-05-26", metrics: { engagementRate: 5.1 } }] }),
"utf8",
);
const { stdout, code } = runCli(root, ["assemble"]);
assert.equal(code, 0);
assert.match(stdout, /aaaaaaaaaaaa/, "surfaces the specific id");
assert.match(stdout, /high/i, "shows the high-confidence analytics match");
assert.ok(!existsSync(join(root, "brain", "profile.md")), "assemble wrote no profile.md");
});
test("SC9: `assemble` on an empty root degrades cleanly (no crash)", () => {
const { code } = runCli(root, ["assemble"]);
assert.equal(code, 0);
});
});

View file

@ -14,6 +14,8 @@ const baseRec = (body: string): PublishedRecord => ({
published_date: "2026-05-26",
captured_at: "2026-06-23",
source: "manual",
specifics: [],
trends: [],
body,
});
@ -82,3 +84,41 @@ describe("PublishedRecord grammar — parse∘serialize identity (SC2, B1 edge b
assert.throws(() => parsePublishedRecord(corrupt), /provenance/i);
});
});
describe("SB-S3c — raw-material ids on the published record", () => {
const ids2 = ["aaaaaaaaaaaa", "bbbbbbbbbbbb"];
// SC1 — round-trip with non-empty specifics/trends, order preserved.
test("SC1: parse∘serialize round-trips a record carrying specifics/trends (order preserved)", () => {
const rec: PublishedRecord = { ...baseRec("a tagged post"), specifics: ids2, trends: ["cccccccccccc"] };
assert.deepEqual(roundTrip(rec), rec);
});
// SC2 — empty arrays → unchanged 5-line header; a pre-S3c fixture round-trips byte-identically.
test("SC2: empty specifics/trends serialize byte-identically to a pre-S3c record", () => {
const oldText =
"id: 0123456789ab\nprovenance: published\npublished_date: 2026-05-26\n" +
"captured_at: 2026-06-23\nsource: manual\n---\nthe body text";
assert.equal(serializePublishedRecord(parsePublishedRecord(oldText)), oldText);
// and the live empty-array record still emits exactly 5 header lines.
const text = serializePublishedRecord(baseRec("hi"));
assert.equal(text.split("\n---\n")[0].split("\n").length, 5);
});
// SC2b — when present, the new lines are appended AFTER source: and before the sentinel.
test("SC2b: non-empty specifics/trends are appended after source:, before the --- sentinel", () => {
const text = serializePublishedRecord({ ...baseRec("x"), specifics: [ids2[0]], trends: [ids2[1]] });
const header = text.split("\n---\n")[0];
assert.match(header, /source: manual\nspecifics: aaaaaaaaaaaa\ntrends: bbbbbbbbbbbb$/);
});
// SC3 (parse side) — a non-12-hex id in the header throws, never silently dropped.
test("SC3: a malformed specifics/trends id throws on parse", () => {
const bad =
serializePublishedRecord(baseRec("x")).replace(
"source: manual",
"source: manual\nspecifics: NOTAHEXID",
);
assert.throws(() => parsePublishedRecord(bad), /bad specifics id/i);
});
});

View file

@ -77,7 +77,7 @@ describe("ingest IO — writePublished / ingestText / scanInbox / listPublished
mkdirSync(publishedDir(root), { recursive: true });
const squatter: PublishedRecord = {
id, provenance: "published", published_date: CAPTURED, captured_at: CAPTURED,
source: "manual", body: "a DIFFERENT body that happens to share the id",
source: "manual", specifics: [], trends: [], body: "a DIFFERENT body that happens to share the id",
};
writeFileSync(join(publishedDir(root), `${id}.md`), serializePublishedRecord(squatter), "utf8");
@ -125,4 +125,34 @@ describe("ingest IO — writePublished / ingestText / scanInbox / listPublished
assert.equal(res.records[0].provenance, "published");
assert.equal(res.skipped, 1, "the malformed file counted as skipped, not a crash");
});
// SC4 — ingestText threads specifics/trends onto the written record; absent → [].
test("SC4: ingestText threads specifics/trends; round-trips off disk", () => {
const spec = "aaaaaaaaaaaa";
const trend = "bbbbbbbbbbbb";
const res = ingestText({ body: "tagged post body", captured_at: CAPTURED, specifics: [spec], trends: [trend] });
assert.deepEqual(res.record.specifics, [spec]);
assert.deepEqual(res.record.trends, [trend]);
const onDisk = parsePublishedRecord(readFileSync(res.path, "utf8"));
assert.deepEqual(onDisk.specifics, [spec]);
assert.deepEqual(onDisk.trends, [trend]);
});
test("SC4: ingestText with no specifics/trends keeps them empty (regression)", () => {
const res = ingestText({ body: "plain post", captured_at: CAPTURED });
assert.deepEqual(res.record.specifics, []);
assert.deepEqual(res.record.trends, []);
});
// SC3 (producer side) — a malformed id thrown fast at the producer boundary.
test("SC3: ingestText with a non-12-hex specifics id throws at the producer", () => {
assert.throws(
() => ingestText({ body: "x", captured_at: CAPTURED, specifics: ["NOTAHEX"] }),
/bad specifics id/i,
);
assert.throws(
() => ingestText({ body: "y", captured_at: CAPTURED, trends: ["zzz"] }),
/bad trends id/i,
);
});
});