The LAST S3 slice: (b) retire the dead, zero-reader content-history.md across
its 8 plumbing surfaces; (c) a read-side `brain reconcile` joining silo 1
(## Recent Posts, auto-tracked creation) to the silo 2↔3 graph — surfacing the
coverage gap (posts created but never `brain ingest`-ed), without writing the
state silo.
Light-Voyage hardened: scope-guardian ALIGNED (0/0/0); brief-reviewer REVISE
(4 FIX) + plan-critic REVISE (3 blockers + 6 major + 4 minor), all folded. The
three blockers were real factual errors against the code, each verified directly
before folding: listPublished() is body-less (use runAssemble's inline
parsePublishedRecord loader); the core signature must be {recentPosts, records,
graph} (PostGraphNode has no body); the parser tracks the WRITER format
(state-updater.mjs:116), not pruneContentHistory's date-only :145 regex. Plus the
getStateFile() HOME-chain seam, logic-RED, single-literal gate decoys, recorded
gate TOTAL 97.
Go-before-code gate pending (forks: retire vs back-fill; read-side vs write-side).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
21 KiB
Brief — SB-S3e: hygiene + triple-post reconciliation (the LAST S3 slice)
Slice: SB-S3e — the focused finale of SB-S3. Carries the two coupled deliverables the S3d split deferred: (b) retire the dead
content-history.md+ (c) triple-post reconciliation (silo 1 post-tracking ↔ silo 2 brain published-records ↔ silo 3 analytics). After this slice the second-brain arc is complete (S4 EØS DMA-connector is optional). Status: DRAFT (brief + plan written; light-Voyage hardening + go-before-code gate pending). Predecessors: SB-S0 (scaffold seeds the bare tree) · SB-S1 (ingest/published/gold-signal records = silo 2) · SB-S3c (assemble.tsbuilt the silo 2↔3 join; deliberately did NOT touch silo 1) · SB-S3d (the ops-centre reader — independent).
1. Operator decision context (2026-06-23)
The original S3d charter bundled three deliverables; the S3d brief (§1) found a clean fault line and split: (a) the ops centre shipped as S3d (additive, no silo contact); (b) + (c) are coupled and move here, to S3e. They are coupled because the content-history back-fill question is the reconciliation question: content-history.md was a second auto-written record of created posts, so deciding its fate (retire vs resurrect) and deciding how the auto-tracked creation stream reconciles to the brain graph are the same decision. Both touch the loose, regex-mutated post-tracking silo that S3c deliberately avoided. S3e is the deliberate, isolated finale for that surgery.
2. The gap — grounded in code
A "post created via the plugin" leaves three separate records, and they do not agree:
| # | Silo | Path / writer | Carries | Join key? |
|---|---|---|---|---|
| 1 | post-tracking | ## Recent Posts in $HOME/.claude/linkedin-studio.local.md — auto-written by the Stop-hook → updatePostTracking (state-updater.mjs:114-126) |
[date] "hook (≤60c preview)" (charCount) - topic |
none |
| 2 | brain published-record | ingest/published/<contentId>.md under the brain dataRoot — written only by manual brain ingest --file … (cli.ts:122-138) |
contentId, published_date, specifics[], trends[], full body |
contentId (internal) |
| 3 | analytics | analytics/posts/*.json under the brain dataRoot — from the LinkedIn CSV import |
title, publishedDate, metrics |
none (no body, no URN) |
S3c built the silo 2↔3 join (assemble.ts: assemblePostGraph) by a confidence-tiered title-prefix + date heuristic, answering the arc's north-star query "which raw material actually performs?" (specifics → post → measured analytics).
Two gaps remain, and they are the same coin:
- Silo 1 is joined to nothing. The complete auto-captured stream of created posts (silo 1) never meets the graph. The graph (silo 2→3) sees only the subset the user manually ran
brain ingeston — so the "which raw material performs" payoff is blind to every post the user created but never hand-ingested. Verified: silo 2 has no auto-capture —assemble's empty-state message literally instructs the user tobrain ingest --file …manually (cli.ts:182); STATE lists auto-capture as the explicit optional S3c-bonus, i.e. it does not exist. - The dead
content-history.mdis a fourth, genuinely-orphaned record (date/hook/topic/format/words/chars/source), written by flaky Stop-hook prose (state-update-reminder.md:67-86) and read by nobody (verified: the onlycontent-historymentions in code are amigrate-data.mjsMOVE entry, its test, the.gitignore/SC2 data-class guards, and asession-start.mjscomment — zero readers). It is redundant with silo 1 (both auto-written, both unread by the graph) and resurrecting it would solve nothing the join needs. Honesty nuance (the prose lies): the writer prose being deleted claims the log "enables/linkedin:reportandanalytics-interpreterto track content production" (state-update-reminder.md:85) — but that was never wired (grep-verified: no command/agent reads the file; theprofile.md/analyze.md/audit.md"content history" hits are the conceptual "have you posted on this topic" signal, not file reads). The claim is aspirational dead prose, which strengthens retirement — but it must be named so a mid-deletion reader doesn't chase a phantom reader.
3. Scope — what is IN (S3e)
(b) Retire the dead content-history.md — clean deletion across its plumbing
Verified surfaces (grounded this session):
- Writer prose — remove Section 5 "Content History Log" from
hooks/prompts/state-update-reminder.md:67-86(the flaky generator). This is the only thing that ever wrote the file. - Template — delete
config/content-history.template.md. - Migration — remove the B1 entry from
MOVE_FILESinhooks/scripts/migrate-data.mjs:33(['assets/analytics/content-history.md', 'analytics/content-history.md']). - Migration test — drop the content-history fixture write (
__tests__/migrate-data.test.mjs:37), its two assertions (:56,:61), and trim the test-(a) description (:50). Test count unchanged (5 cases a–e stay) → hook-suite total unchanged. .gitignore:44— removeassets/analytics/content-history.md.- Gate guard — remove the
assets/analytics/content-history\.mdalternative fromSC2_CLASSES(test-runner.sh:592). The SC2 check stays (one pass/fail) → gate total unchanged. - Reference doc — remove
content-history.mdfrom the data-tree diagram (references/data-path-convention.md:26). - Comment — drop
content-historyfrom thesession-start.mjs:94comment list.
NOT in scope of (b): state-updater.mjs's pruneContentHistory (despite the name, it operates on the live ## Recent Posts in the state file — :138-172 — and is untouched), and any conceptual "content history" prose (e.g. README:120 /linkedin:profile "content-history alignment" reads as the concept, not the file — verify-then-leave; never delete a conceptual mention).
(c) Triple-post reconciliation — read-side joiner (recommended fork)
Extend the brain's read-only assembler so silo 1 is reconciled against the graph, without writing the state file (faithful to S3c's "read, never write").
- A new pure reconcile core (
scripts/brain/src/reconcile.ts):reconcileRecentPosts({ recentPosts, records, graph })— takes the parsed silo-1 entries + the fullPublishedRecord[](which carrybody) + the existingPostGraphNode[], and returns, per silo-1 entry, whether it is present in the brain (a matched published record) and/or analytics, with an explicit confidence tier — surfacing the coverage gap: created posts that never reached the brain graph. Whyrecordsand not graph-only:PostGraphNodecarriescontentId+ analytics match but nobody(assemble.ts:48-54), so the hook→post prefix-match needs thePublishedRecord.body; the core matcheshook → record.body, thenrecord.id → graph nodefor the analytics tier. No FS/clock/network in the core (theassemble.tsdiscipline). - A thin read-only state-file parser (
parseRecentPosts(stateText)): extract[date] "hook" (chars) - topicrows from the## Recent Postssection. The format source of truth is the writerupdatePostTracking(state-updater.mjs:116:- [date] "hook" (chars) - topic) — notpruneContentHistory's regex, which is date-only (:145,/^- \[(\d{4}-\d{2}-\d{2})\] .+$/gm) and captures no hook/chars/topic.parseRecentPostsis the first full-field reader of this section; it shares only the date-anchor shape with the pruner. A golden-string test pins a writer-produced entry round-tripping throughparseRecentPosts. - A new brain IO seam for the state file. Silo 1 lives at
STATE_FILE || $HOME/.claude/linkedin-studio.local.md— outside the brain dataRoot (LINKEDIN_STUDIO_DATA). A newloadRecentPosts()IO resolves that path with the canonical precedence already exported asdata-root.mjs: getStateFile()—STATE_FILEfirst, elsejoin(resolveHome(), '.claude', 'linkedin-studio.local.md')whereresolveHome() = HOME || USERPROFILE || homedir()(NOT barehomedir()— that would ignore aHOME/USERPROFILEoverride the writer honours). Reads read-only; absent →[](fresh-clone safe). This is a genuinely new root-skew caveat (the state file resolves viaSTATE_FILE, a different root than the brain dataRoot — not a mirror ofassemble.ts'sANALYTICS_ROOTnote, which is about the analytics dir underLINKEDIN_STUDIO_DATA). (A hook-side.mjsreconciler is pathguard-blocked — new.mjsunderhooks/scripts/is forbidden — so the brain TS side is the only clean home; it copiesgetStateFile()'s chain since it cannot import the.mjs.) - A CLI subcommand
brain reconcile(cli.ts): loads silo 1 + the assembled graph, prints the per-post coverage report (in brain / in analytics / orphaned-in-state), and a one-line summary ("N created posts, M in the brain graph, K un-ingested →brain ingestthem to feed the graph"). Read-only. - Deterministic gate section (new Section, modelled on 16e/16d): unconditional checks that the reconcile wiring exists (CLI subcommand registered; core exported).
ASSERT_BASELINE_FLOORbumped by exactly the new unconditional checks. - Brain-suite tests (RED first):
reconcile.test.ts— coverage tiers (present/absent/orphaned), the truncated-hook prefix limit, empty-state degradation.BRAIN_TESTS_FLOORbumped by the new passing count (recorded live, not assumed).
Honest limits of read-side (documented, not hidden): (i) silo 1 carries only a ≤60-char truncated hook preview, a weaker prefix signal than silo 2's full body → lower match confidence, reported honestly as a tier, never as proof. (ii) Read-side cannot retroactively assign the specifics/trends ids a post was built from — those were never captured for un-ingested posts; the join can only say "this created post is missing from the graph," not reconstruct its raw material. (iii) The true gap-closer is auto-capture at drafting (silo-2 record minted with specifics/trends at creation) — explicitly a follow-up, not S3e (STATE's S3c-bonus). S3e ships the honest read-side reconciliation + the coverage signal that motivates auto-capture next.
4. Non-goals — what is OUT (deferred)
- Write-side stamping — embedding a
contentIdinto the## Recent Postsentry. Rejected for S3e: it requires acontentIdto exist at post-creation, i.e. auto-capture (out of scope), and it mutates the$-injection-defended regex silo (updatePostTrackingalready had to use replacement functions to stay$-safe —state-updater.mjs:117-125). Heavier + riskier + premature. See §8 fork 2. - Auto-capture at drafting (the real gap-closer) — minting a silo-2 published record with specifics/trends ids at post-creation. The motivated follow-up; S3e's coverage report is what makes its value legible.
- Resurrecting
content-history.mdas a live read tributary (the back-fill arm of fork 1) — it would add a second weak auto-record the graph still can't join. Rejected; retirement chosen. - Any change to
pruneContentHistory/## Recent Postswriter /state-updater.mjs— S3e reads silo 1, never writes it. - A second reconcile consumer (a command/agent that acts on the coverage report) — S3e ships the
brain reconcileview; wiring an agent to nudge "ingest these" is follow-on. assemble.tsjoin-heuristic changes — reconcile consumes the existing graph; it does not re-tune the silo 2↔3 matcher.
5. Boundaries / invariants (must hold)
- READ-only into the state silo — S3e adds no write path to
## Recent Postsor any state-file field;state-updater.mjsstays the sole writer of silo 1. - READ-only into generation —
brain consolidate --apply --confirmstays the sole writer ofprofile.md; reconcile writes nothing. - Retirement is plumbing-only — (b) removes a dead file + its writer/migration/guards; it must not touch the live
## Recent Posts,pruneContentHistory, or any conceptual "content history" prose. - No new hook
.mjs— pathguard forbids new.mjsunderhooks/scripts/; reconcile lives inscripts/brain/(TS). Editing the existingmigrate-data.mjs(a deletion) is an Edit of an existing file → allowed. - State-file root precedence mirrored exactly —
loadRecentPosts()resolvesSTATE_FILE || $HOME/.claude/linkedin-studio.local.md, identical to the.mjsside; a divergence would silently read the wrong file. - Fresh-clone / empty-state safe — missing state file or empty
## Recent Posts→ reconcile degrades to a clean empty report, never a crash (mirrorsloadAnalyticsRows). - Migration idempotency intact — removing the B1 MOVE entry must not break
migrate-data's.migrated-marker idempotency or the COPY/MOVE classes; the remaining test cases (b–e) stay green. - TDD iron law — the failing reconcile test (and the failing gate-section self-test) land BEFORE the reconcile code.
6. Success criteria (testable)
- SC1 — content-history retired:
grep -rn "content-history" hooks/ config/ scripts/test-runner.sh .gitignore references/data-path-convention.md(NO--includefilter — it must reach.gitignoreand the.mdref-doc, which an extension-filter would silently skip) returns zero (file, writer, MOVE entry, gitignore line, SC2 alternative, ref-doc, comment all gone); the template file is deleted. The README:120conceptual mention is intentionally outside this grep set (concept, not file). (grep +lsassertion — identical command in the plan, SC1) - SC2 — migration green after retirement:
(cd … && node --test hooks/scripts/__tests__/migrate-data.test.mjs)passes with cases a–e; no content-history assertion remains. (hook suite) - SC3 — reconcile core correct (RED-first, LOGIC-RED not import-RED):
reconcile.test.tsasserts the three coverage tiers (in-graph / in-brain-only / orphaned-in-state), truncated-hook degradation, and empty-state → empty report. RED proof recorded: the artifact must show the named tier assertions failing (e.g. "expectedorphaned-in-statefor the un-ingested entry") against a stubreconcile.tsthat compiles but mis-classifies (returns[]/ wrong tiers) — a bareMODULE_NOT_FOUNDimport-RED proves nothing about the assertion logic and is rejected as vacuous.BRAIN_TESTS_FLOORbumped to the recorded new count. (brain suite) - SC4 — reconcile wired:
brain reconcileruns end-to-end on a seeded fixture (silo 1 + published + analytics) and prints the coverage report + summary; a new gate Section asserts the CLI subcommand + core export exist (unconditional self-test + real-file grep). (lint + manual run) - SC5 — gate green + floor honoured:
scripts/test-runner.shstays green; TOTAL recounted live at land (current 97; + the new unconditional checks).ASSERT_BASELINE_FLOORbumped by exactly that many; SC2-dry-run still passes with the narrowedSC2_CLASSES. - SC6 — root-skew (manual/behavioural — EXPECTED to land as a real pass): with
STATE_FILEpointing at a temp state file,brain reconcilereads silo 1 from it; with it unset it falls back to$HOME/.claude/linkedin-studio.local.md. This is a trivialSTATE_FILEenv toggle in a temp dir — exercisable in the same shell that runs the gate — so the default expected landing state is a real behavioural pass, not a hedge. Fallback (verifiseringsplikt): only if the env toggle genuinely cannot run, record wiring-inspected-only, NOT a behavioural pass — the hedge must never become a standing excuse to skip a runnable check. - SC7 — counts reconciled:
architecture.mdS3 build-row marked S3e ✅ (arc complete); STATE telling updated (gate total, ASSERT floor, brain floor); no CLAUDE.md command/agent/reference-doc count change (no new command/agent;reconcile.tsis a brain module + CLI subcommand, not a plugin surface — confirm at the gate whetherbrain reconcilewarrants a one-line CLAUDE.md mention).
7. Verification
- Deterministic (gate): SC1/SC4/SC5 via grep + the new gate Section in
test-runner.sh; SC2 via the migrate-data hook test; SC3 via the brain suite (cd scripts/brain && npm installbefore the gate, else warn-skip — STATE rule). - Behavioural (manual, documented — the honest limit): SC4 end-to-end run + SC6 root-skew — recorded in STATE/changelog at land; never overclaim a behavioural pass not run (verifiseringsplikt).
8. Open questions for brief-review / the go-gate
- Fork 1 — content-history: retire vs back-fill. Recommended retire — zero readers, redundant with silo 1, and back-fill resurrects a second weak auto-record the graph still cannot join. Confirm.
- Fork 2 — reconciliation: read-side joiner vs write-side stamp. Recommended read-side — faithful to S3c's read-only invariant, avoids the
$-injection regex silo, and write-side is blocked on auto-capture (nocontentIdexists at creation today) so it would be premature. Cost honestly stated: a new cross-seam read (brain TS → state file). Confirm; write-side + auto-capture become the flagged follow-up. - Scope of (c): build the read-side joiner now, or document-the-design + defer the build? Recommended build now — the coverage report is the genuine deliverable that closes the arc and motivates auto-capture; a doc-only finale under-delivers the last slice. Confirm the build.
- CLAUDE.md surface for
brain reconcile. The brain CLI is an internal tool, not a plugin command — recommended: a one-line architecture note, no command-count change. Confirm.
9. Light-Voyage review — folded
Three reviewers ran on the brief + plan (2026-06-23):
voyage:scope-guardian: ALIGNED — 0 creep, 0 gaps, 0 dependency issues. All 14 IN items (b1–b8, c1–c6) map to a plan step; no NON-GOAL file is touched (the scope fence is consistent with the steps, and slightly broader/defensive); doc reconciliation is proportional (architecture build-row + STATE telling only). Every referenced file/line/symbol verified against the live codebase.voyage:brief-reviewer: REVISE → 4 [FIX]es folded:- FIX-1 (load-bearing): the parser must mirror the writer
updatePostTracking(state-updater.mjs:116), NOTpruneContentHistory— whose:145regex is date-only and captures no hook/chars/topic. Folded into §3(c).2 (+ a golden-string round-trip test). The "they never disagree" claim was rebuilt on the writer-format source of truth. ✅ - FIX-2 (honesty): the writer prose being deleted claims
/linkedin:report+analytics-interpreteras readers, but they were never wired — aspirational dead prose. Named in §2 so the "zero readers" verdict is auditable, not asserted. ✅ - FIX-3 (testability): SC3 RED tightened to a logic-RED (named tier assertions fail against a mis-classifying stub), rejecting a vacuous import-RED. ✅
- FIX-4 (testability + consistency): SC6 pre-committed as an expected real behavioural pass (trivial env toggle), wiring-only as documented fallback; and the root-skew caveat is genuinely new (state file via
STATE_FILE), not a mirror ofassemble.ts'sANALYTICS_ROOTnote — citedata-root.mjs: getStateFile()as the canonical precedence. Folded into §3(c).3 + SC6. ✅ - Also folded: the core signature corrected to
{ recentPosts, records, graph }(graph-only cannot prefix-match —PostGraphNodehas nobody), and SC1's grep unified with the plan's (no--includefilter, which would skip.gitignore/.md).
- FIX-1 (load-bearing): the parser must mirror the writer
voyage:plan-critic: REVISE (3 blockers + 6 major + 4 minor) — folded intoplan-sb-s3e.md§"Plan-critic — folded". The three blockers were real factual errors against the code, each verified directly before folding: (B1)listPublished()is body-less → userunAssemble's inlineparsePublishedRecordloader (cli.ts:162-176, which already builds abodyByIdMap); (B2) the core signature contradiction (= the brief's signature fold); (B3) the:145-regex misattribution (= FIX-1). Majors: thegetStateFile()HOME-chain (not barehomedir()), single-literal gate decoys, Check-B literal-name constraint, recorded gate TOTAL (verified live 97), demonstrated migrate idempotency, and the SC1 grep unification.
Net: scope unchanged (ALIGNED); the folds correct three load-bearing factual errors in the reconcile design (body-less loader, wrong regex, wrong HOME seam) + harden the RED guarantee and the gate's non-vacuity. The retirement arm (b) was already nearly executable; the reconcile arm (c) is now grounded in the actual assemble.ts/ingest.ts/cli.ts shapes.