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
210 lines
8.6 KiB
TypeScript
210 lines
8.6 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|