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

@ -26,6 +26,7 @@ import {
type ProfileDiff,
} from "./consolidate.js";
import { assemblePostGraph, loadAnalyticsRows } from "./assemble.js";
import { loadRecentPosts, reconcileRecentPosts, summarizeReconcile } from "./reconcile.js";
import { dataRoot } from "./dataRoot.js";
import { ingestText, listPublished, parsePublishedRecord, scanInbox } from "./ingest.js";
import { parseProfile, serializeProfile } from "./profile.js";
@ -91,7 +92,8 @@ function usage(msg: string): never {
" ingest --file <path> [--source <s>] [--date <YYYY-MM-DD>] [--specific <id>]… [--trend <id>]…\n" +
" ingest --scan-inbox [--source <s>]\n" +
" published list [--json]\n" +
" assemble",
" assemble\n" +
" reconcile",
);
process.exit(2);
}
@ -200,6 +202,52 @@ function runAssemble(_flags: Record<string, string>): void {
}
}
/**
* SB-S3e: read-only triple-post reconciliation silo 1 (`## Recent Posts`,
* auto-tracked creation) vs the silo 23 graph. Surfaces the coverage gap
* (created posts never `brain ingest`-ed). Writes NOTHING. Loads real
* `PublishedRecord[]` (with `body`) via the same inline parse as `runAssemble`
* NOT `listPublished` (body-less). Calls the core by its literal name.
*/
function runReconcile(_flags: Record<string, string>): void {
const pubDir = dataRoot(join("ingest", "published"));
const records = existsSync(pubDir)
? readdirSync(pubDir)
.filter((f) => f.endsWith(".md") && !f.startsWith("."))
.map((f) => {
try {
return parsePublishedRecord(readFileSync(join(pubDir, f), "utf8"));
} catch {
return null;
}
})
.filter((r): r is NonNullable<typeof r> => r !== null)
: [];
const analytics = loadAnalyticsRows();
const graph = assemblePostGraph({ records, analytics });
const recentPosts = loadRecentPosts();
const nodes = reconcileRecentPosts({ recentPosts, records, graph });
const sum = summarizeReconcile(nodes);
if (nodes.length === 0) {
console.log(
"No tracked posts in `## Recent Posts` to reconcile (check that posts were created via the plugin, or that STATE_FILE points at your state file).",
);
return;
}
console.log(
`Reconcile — ${sum.created} created · ${sum.inGraph} in-graph · ${sum.inBrainOnly} in-brain-only · ${sum.orphaned} orphaned:`,
);
for (const n of nodes) {
const id = n.contentId ? ` · ${n.contentId}` : "";
console.log(`\${n.recentPost.date} · ${n.tier}${id}\n "${n.recentPost.hook}"`);
}
if (sum.orphaned > 0) {
console.log(
`\n${sum.orphaned} created post(s) are not in the brain graph — \`brain ingest --file <p> [--specific <id>] [--trend <id>]\` to feed them.`,
);
}
}
function renderDiffMd(diff: ProfileDiff): string {
const lines = ["# Pending profile diff", "", "> Operator-gated. Review, then `brain consolidate --apply --diff brain/pending-diff.json --confirm`.", ""];
const section = (title: string, items: string[]) => {
@ -310,6 +358,7 @@ function main(): void {
if (command === "published") return runPublished(rest, flags);
if (command === "consolidate") return runConsolidate(flags);
if (command === "assemble") return runAssemble(flags);
if (command === "reconcile") return runReconcile(flags);
usage(command ? `unknown command: ${command}` : "no command given");
}