feat(linkedin-studio): SB-S3e — retire dead content-history + read-side brain reconcile [skip-docs]

The LAST second-brain slice; the S0–S3e arc is now complete.

(b) Retire the dead, zero-reader content-history.md across its 8 plumbing
surfaces: the flaky Stop-hook writer prose, the template (git-rm'd), the
migrate-data.mjs B1 MOVE entry + its test assertions, .gitignore, the gate
SC2_CLASSES guard, the data-path ref-doc, and a session-start comment. SC1
grep = 0; migrate suite 5/5 (R8 idempotency demonstrated).

(c) `brain reconcile` — read-side triple-post reconciliation joining silo 1
(## Recent Posts, auto-tracked creation) to the silo 2↔3 graph, surfacing the
coverage gap: posts created via the plugin but never `brain ingest`-ed. New
pure core scripts/brain/src/reconcile.ts (parseRecentPosts tracks the WRITER
format state-updater.mjs:116, NOT the date-only pruner; reconcileRecentPosts
matches hook→record.body→graph for the in-graph/in-brain-only/orphaned tiers;
loadRecentPosts reads STATE_FILE via the canonical getStateFile() chain — a new
cross-seam read, the state file lives outside the brain dataRoot). Wired as the
`brain reconcile` CLI subcommand (inline parsePublishedRecord loader, not the
body-less listPublished). Read-only: never writes the state silo.

Tests (verified live): gate 99/0/0 (ASSERT floor 82→84; new Section 16f
self-test + CLI grep) · brain 127/127 (floor 114→127, +13 reconcile) · hook
suite 136/136. SC4/SC6 end-to-end run is a real behavioural pass (STATE_FILE
seam read + fallback). Honest limit: read-side cannot reconstruct un-captured
specifics/trends — auto-capture is the flagged follow-up.

Brief/plan: docs/second-brain/{brief,plan}-sb-s3e.md (fbad29d). Go-before-code
gate cleared (operator: retire · read-side · build-now).

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 22:13:33 +02:00
commit 68f6283d8a
12 changed files with 487 additions and 54 deletions

View file

@ -0,0 +1,210 @@
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { mintContentId } from "../src/id.js";
import type { PublishedRecord } from "../src/ingest.js";
import { assemblePostGraph, type AnalyticsRowInput } from "../src/assemble.js";
import {
parseRecentPosts,
reconcileRecentPosts,
summarizeReconcile,
loadRecentPosts,
} from "../src/reconcile.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 };
}
// Bodies whose normalized opener is ≥ HOOK_PREFIX_FLOOR (24) chars.
const BODY_A = "Jeg lærte noe viktig om dømmekraft i dag.\n\nDel 1 av serien.";
const HOOK_A = "Jeg lærte noe viktig om dømmekraft i dag.";
const BODY_B = "En helt annen tekst om automatisering og styring.\n\nDel 2.";
const HOOK_B = "En helt annen tekst om automatisering og styring.";
// The writer's exact entry format (state-updater.mjs:116): - [date] "hook" (chars) - topic
function entry(date: string, hook: string, chars: number, topic: string): string {
return `- [${date}] "${hook}" (${chars}) - ${topic}`;
}
function stateWith(...entries: string[]): string {
return [
"---",
"posts_this_week: 3",
"---",
"",
"## Recent Posts",
"",
...entries,
"",
"## Milestone Log",
"",
"- [2026-05] 1048 (+10)",
"",
].join("\n");
}
describe("SB-S3e — parseRecentPosts (writer-format reader)", () => {
test("extracts {date, hook, charCount, topic} from the ## Recent Posts section", () => {
const text = stateWith(
entry("2026-05-26", HOOK_A, 1400, "dømmekraft"),
entry("2026-05-20", HOOK_B, 900, "automatisering"),
);
const posts = parseRecentPosts(text);
assert.equal(posts.length, 2);
assert.deepEqual(posts[0], { date: "2026-05-26", hook: HOOK_A, charCount: 1400, topic: "dømmekraft" });
assert.deepEqual(posts[1], { date: "2026-05-20", hook: HOOK_B, charCount: 900, topic: "automatisering" });
});
test("golden-string: a literal writer-produced entry round-trips verbatim", () => {
// The byte-exact string updatePostTracking emits (state-updater.mjs:116).
const writerLine = `- [2026-06-01] "${HOOK_A}" (1234) - strategi`;
const posts = parseRecentPosts(["## Recent Posts", "", writerLine, ""].join("\n"));
assert.equal(posts.length, 1);
assert.equal(posts[0].hook, HOOK_A);
assert.equal(posts[0].charCount, 1234);
assert.equal(posts[0].topic, "strategi");
});
test("stops at the next section (## Milestone Log is not parsed as an entry)", () => {
const text = stateWith(entry("2026-05-26", HOOK_A, 1400, "dømmekraft"));
const posts = parseRecentPosts(text);
assert.equal(posts.length, 1);
assert.ok(posts.every((p) => /^\d{4}-\d{2}-\d{2}$/.test(p.date)));
});
test("a doc with no ## Recent Posts section → []", () => {
assert.deepEqual(parseRecentPosts("# Some other doc\n\n## Notes\n\n- a line"), []);
});
test("$-bearing hook/topic round-trips verbatim (capture on read — no replace, no injection)", () => {
const dollarHook = "Hvorfor $100 i budsjettkutt endrer alt for $deg";
const text = ["## Recent Posts", "", entry("2026-05-26", dollarHook, 800, "$penger$"), ""].join("\n");
const posts = parseRecentPosts(text);
assert.equal(posts.length, 1);
assert.equal(posts[0].hook, dollarHook);
assert.equal(posts[0].topic, "$penger$");
});
});
describe("SB-S3e — reconcileRecentPosts (coverage tiers)", () => {
test("in-graph: created post matches a published record that HAS analytics", () => {
const r = rec(BODY_A);
const graph = assemblePostGraph({ records: [r], analytics: [row(HOOK_A)] });
const recentPosts = parseRecentPosts(
["## Recent Posts", "", entry("2026-05-26", HOOK_A, 1400, "dømmekraft"), ""].join("\n"),
);
const nodes = reconcileRecentPosts({ recentPosts, records: [r], graph });
assert.equal(nodes.length, 1);
assert.equal(nodes[0].tier, "in-graph");
assert.equal(nodes[0].contentId, r.id);
});
test("in-brain-only: created post matches a record but the record has NO analytics", () => {
const r = rec(BODY_B);
const graph = assemblePostGraph({ records: [r], analytics: [] }); // no analytics → match none
const recentPosts = parseRecentPosts(
["## Recent Posts", "", entry("2026-05-26", HOOK_B, 900, "automatisering"), ""].join("\n"),
);
const nodes = reconcileRecentPosts({ recentPosts, records: [r], graph });
assert.equal(nodes[0].tier, "in-brain-only");
assert.equal(nodes[0].contentId, r.id);
});
test("orphaned-in-state: created post matches NO published record (never ingested)", () => {
const r = rec(BODY_A);
const graph = assemblePostGraph({ records: [r], analytics: [row(HOOK_A)] });
const recentPosts = parseRecentPosts(
["## Recent Posts", "", entry("2026-05-19", "En post som aldri ble ingestet i hjernen", 700, "annet"), ""].join("\n"),
);
const nodes = reconcileRecentPosts({ recentPosts, records: [r], graph });
assert.equal(nodes[0].tier, "orphaned-in-state");
assert.equal(nodes[0].contentId, undefined);
});
test("truncated-hook (…) degradation: a '…'-suffixed preview still prefix-matches", () => {
const r = rec(BODY_A);
const graph = assemblePostGraph({ records: [r], analytics: [row(HOOK_A)] });
const recentPosts = parseRecentPosts(
["## Recent Posts", "", entry("2026-05-26", "Jeg lærte noe viktig om dømmekraft i…", 1400, "dømmekraft"), ""].join("\n"),
);
const nodes = reconcileRecentPosts({ recentPosts, records: [r], graph });
assert.equal(nodes[0].tier, "in-graph");
});
test("below-floor hook → orphaned (too short to discriminate, never a false match)", () => {
const r = rec(BODY_A);
const graph = assemblePostGraph({ records: [r], analytics: [row(HOOK_A)] });
const recentPosts = parseRecentPosts(
["## Recent Posts", "", entry("2026-05-26", "Jeg lærte", 1400, "dømmekraft"), ""].join("\n"),
);
const nodes = reconcileRecentPosts({ recentPosts, records: [r], graph });
assert.equal(nodes[0].tier, "orphaned-in-state");
});
test("summary counts created / in-graph / in-brain-only / orphaned", () => {
const rA = rec(BODY_A);
const rB = rec(BODY_B);
const graph = assemblePostGraph({ records: [rA, rB], analytics: [row(HOOK_A)] });
const recentPosts = parseRecentPosts(
[
"## Recent Posts",
"",
entry("2026-05-26", HOOK_A, 1400, "a"), // in-graph (rA has analytics)
entry("2026-05-26", HOOK_B, 900, "b"), // in-brain-only (rB no analytics)
entry("2026-05-10", "Helt ukjent post som ingen record dekker", 600, "c"), // orphaned
"",
].join("\n"),
);
const nodes = reconcileRecentPosts({ recentPosts, records: [rA, rB], graph });
const sum = summarizeReconcile(nodes);
assert.deepEqual(sum, { created: 3, inGraph: 1, inBrainOnly: 1, orphaned: 1 });
});
test("empty ## Recent Posts → empty report, no throw", () => {
const nodes = reconcileRecentPosts({ recentPosts: [], records: [rec(BODY_A)], graph: [] });
assert.deepEqual(nodes, []);
assert.deepEqual(summarizeReconcile(nodes), { created: 0, inGraph: 0, inBrainOnly: 0, orphaned: 0 });
});
});
describe("SB-S3e — loadRecentPosts (state-file IO seam, STATE_FILE precedence)", () => {
test("reads the state file pointed at by STATE_FILE; unset/absent → []", () => {
const dir = mkdtempSync(join(tmpdir(), "ls-recon-"));
const file = join(dir, "linkedin-studio.local.md");
writeFileSync(file, stateWith(entry("2026-05-26", HOOK_A, 1400, "dømmekraft")));
const prev = process.env.STATE_FILE;
try {
process.env.STATE_FILE = file;
const posts = loadRecentPosts();
assert.equal(posts.length, 1);
assert.equal(posts[0].hook, HOOK_A);
// Absent file via STATE_FILE → [] (fresh-clone safe, no throw).
process.env.STATE_FILE = join(dir, "does-not-exist.md");
assert.deepEqual(loadRecentPosts(), []);
} finally {
if (prev === undefined) delete process.env.STATE_FILE;
else process.env.STATE_FILE = prev;
rmSync(dir, { recursive: true, force: true });
}
});
});