docs(linkedin-studio): SB-S3e brief + plan — hygiene + triple-post reconciliation (light-Voyage hardened)
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
This commit is contained in:
parent
974e8d1b25
commit
fbad29d3d4
2 changed files with 271 additions and 0 deletions
113
docs/second-brain/brief-sb-s3e.md
Normal file
113
docs/second-brain/brief-sb-s3e.md
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# 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.ts` built 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 ingest` on — 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 to `brain 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.md`** is 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 only `content-history` mentions in code are a `migrate-data.mjs` MOVE entry, its test, the `.gitignore`/SC2 data-class guards, and a `session-start.mjs` *comment* — 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:report` and `analytics-interpreter` to track content production" (`state-update-reminder.md:85`) — but that was **never wired** (grep-verified: no command/agent reads the file; the `profile.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):
|
||||||
|
|
||||||
|
1. **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.
|
||||||
|
2. **Template** — delete `config/content-history.template.md`.
|
||||||
|
3. **Migration** — remove the B1 entry from `MOVE_FILES` in `hooks/scripts/migrate-data.mjs:33` (`['assets/analytics/content-history.md', 'analytics/content-history.md']`).
|
||||||
|
4. **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.
|
||||||
|
5. **`.gitignore:44`** — remove `assets/analytics/content-history.md`.
|
||||||
|
6. **Gate guard** — remove the `assets/analytics/content-history\.md` alternative from `SC2_CLASSES` (`test-runner.sh:592`). The SC2 check stays (one pass/fail) → **gate total unchanged**.
|
||||||
|
7. **Reference doc** — remove `content-history.md` from the data-tree diagram (`references/data-path-convention.md:26`).
|
||||||
|
8. **Comment** — drop `content-history` from the `session-start.mjs:94` comment 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").
|
||||||
|
|
||||||
|
1. **A new pure reconcile core** (`scripts/brain/src/reconcile.ts`): `reconcileRecentPosts({ recentPosts, records, graph })` — takes the parsed silo-1 entries + the **full `PublishedRecord[]`** (which carry `body`) + the existing `PostGraphNode[]`, 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. **Why `records` and not graph-only:** `PostGraphNode` carries `contentId` + analytics match but **no `body`** (`assemble.ts:48-54`), so the hook→post prefix-match needs the `PublishedRecord.body`; the core matches `hook → record.body`, then `record.id → graph node` for the analytics tier. No FS/clock/network in the core (the `assemble.ts` discipline).
|
||||||
|
2. **A thin read-only state-file parser** (`parseRecentPosts(stateText)`): extract `[date] "hook" (chars) - topic` rows from the `## Recent Posts` section. The format source of truth is the **writer** `updatePostTracking` (`state-updater.mjs:116`: `- [date] "hook" (chars) - topic`) — **not** `pruneContentHistory`'s regex, which is **date-only** (`:145`, `/^- \[(\d{4}-\d{2}-\d{2})\] .+$/gm`) and captures no hook/chars/topic. `parseRecentPosts` is 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 through `parseRecentPosts`.
|
||||||
|
3. **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 new `loadRecentPosts()` IO resolves that path with the canonical precedence already exported as `data-root.mjs: getStateFile()` — `STATE_FILE` first, else `join(resolveHome(), '.claude', 'linkedin-studio.local.md')` where `resolveHome() = HOME || USERPROFILE || homedir()` (NOT bare `homedir()` — that would ignore a `HOME`/`USERPROFILE` override the writer honours). Reads read-only; absent → `[]` (fresh-clone safe). This is a **genuinely new root-skew caveat** (the state file resolves via `STATE_FILE`, a *different* root than the brain dataRoot — not a mirror of `assemble.ts`'s `ANALYTICS_ROOT` note, which is about the analytics *dir* under `LINKEDIN_STUDIO_DATA`). (A hook-side `.mjs` reconciler is **pathguard-blocked** — new `.mjs` under `hooks/scripts/` is forbidden — so the brain TS side is the only clean home; it copies `getStateFile()`'s chain since it cannot import the `.mjs`.)
|
||||||
|
4. **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 ingest` them to feed the graph"). Read-only.
|
||||||
|
5. **Deterministic gate section** (new Section, modelled on 16e/16d): unconditional checks that the reconcile wiring exists (CLI subcommand registered; core exported). `ASSERT_BASELINE_FLOOR` bumped by exactly the new unconditional checks.
|
||||||
|
6. **Brain-suite tests** (RED first): `reconcile.test.ts` — coverage tiers (present/absent/orphaned), the truncated-hook prefix limit, empty-state degradation. `BRAIN_TESTS_FLOOR` bumped 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 `contentId` into the `## Recent Posts` entry. Rejected for S3e: it requires a `contentId` to *exist* at post-creation, i.e. **auto-capture** (out of scope), and it mutates the `$`-injection-defended regex silo (`updatePostTracking` already 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.md`** as 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 Posts` writer / `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 reconcile` view; wiring an agent to nudge "ingest these" is follow-on.
|
||||||
|
- **`assemble.ts` join-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 Posts` or any state-file field; `state-updater.mjs` stays the sole writer of silo 1.
|
||||||
|
- **READ-only into generation** — `brain consolidate --apply --confirm` stays the sole writer of `profile.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 `.mjs` under `hooks/scripts/`; reconcile lives in `scripts/brain/` (TS). Editing the existing `migrate-data.mjs` (a deletion) is an Edit of an existing file → allowed.
|
||||||
|
- **State-file root precedence mirrored exactly** — `loadRecentPosts()` resolves `STATE_FILE || $HOME/.claude/linkedin-studio.local.md`, identical to the `.mjs` side; 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 (mirrors `loadAnalyticsRows`).
|
||||||
|
- **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 `--include` filter — it must reach `.gitignore` and the `.md` ref-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 `:120` *conceptual* mention is intentionally outside this grep set (concept, not file). *(grep + `ls` assertion — 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.ts` asserts 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. "expected `orphaned-in-state` for the un-ingested entry") against a **stub `reconcile.ts` that compiles but mis-classifies** (returns `[]` / wrong tiers) — a bare `MODULE_NOT_FOUND` import-RED proves nothing about the assertion logic and is rejected as vacuous. `BRAIN_TESTS_FLOOR` bumped to the recorded new count. *(brain suite)*
|
||||||
|
- **SC4 — reconcile wired:** `brain reconcile` runs 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.sh` stays green; TOTAL recounted **live** at land (current 97; + the new unconditional checks). `ASSERT_BASELINE_FLOOR` bumped by exactly that many; SC2-dry-run still passes with the narrowed `SC2_CLASSES`.
|
||||||
|
- **SC6 — root-skew (manual/behavioural — EXPECTED to land as a real pass):** with `STATE_FILE` pointing at a temp state file, `brain reconcile` reads silo 1 from it; with it unset it falls back to `$HOME/.claude/linkedin-studio.local.md`. This is a trivial `STATE_FILE` env 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.md` S3 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.ts` is a brain module + CLI subcommand, not a plugin surface — confirm at the gate whether `brain reconcile` warrants 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 install` before 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
|
||||||
|
|
||||||
|
1. **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.
|
||||||
|
2. **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 (no `contentId` exists 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.
|
||||||
|
3. **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.
|
||||||
|
4. **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`), NOT `pruneContentHistory` — whose `:145` regex 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-interpreter` as 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 of `assemble.ts`'s `ANALYTICS_ROOT` note — cite `data-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 — `PostGraphNode` has no `body`), and **SC1's grep** unified with the plan's (no `--include` filter, which would skip `.gitignore`/`.md`).
|
||||||
|
- **`voyage:plan-critic`: REVISE (3 blockers + 6 major + 4 minor)** — folded into `plan-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 → use `runAssemble`'s inline `parsePublishedRecord` loader (`cli.ts:162-176`, which already builds a `bodyById` Map); (B2) the core signature contradiction (= the brief's signature fold); (B3) the `:145`-regex misattribution (= FIX-1). Majors: the `getStateFile()` HOME-chain (not bare `homedir()`), 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.
|
||||||
158
docs/second-brain/plan-sb-s3e.md
Normal file
158
docs/second-brain/plan-sb-s3e.md
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
# Plan — SB-S3e: hygiene (retire dead `content-history.md`) + triple-post reconciliation (read-side)
|
||||||
|
|
||||||
|
> Brief: `docs/second-brain/brief-sb-s3e.md`. Slice: SB-S3e — the LAST S3 slice (arc complete after; S4 optional).
|
||||||
|
> Recommendations pending the go-gate: **(b) retire** content-history · **(c) read-side joiner** (`brain reconcile`) · build now (not doc-only) · brain-CLI-only (no new plugin command/agent).
|
||||||
|
> Order is TDD: the failing reconcile test + gate self-test land BEFORE the reconcile code (iron law). The retirement is deletion — its proof is grep-zero + the migrate suite staying green.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Two coupled deliverables: **(b)** the dead, zero-reader `content-history.md` is removed across all eight of its plumbing surfaces (writer prose, template, migration entry+test, gitignore, gate guard, ref-doc, comment); **(c)** a new read-only `brain reconcile` reconciles silo 1 (`## Recent Posts`, auto-tracked creation) against the silo 2↔3 graph, surfacing the coverage gap (created posts never ingested into the brain) — without ever writing the state silo. No state-file write, no `$`-injection surface, no auto-capture.
|
||||||
|
|
||||||
|
## Files touched (exhaustive — for scope-guardian)
|
||||||
|
|
||||||
|
### (b) Retirement
|
||||||
|
|
||||||
|
| File | Change | SC |
|
||||||
|
|------|--------|-----|
|
||||||
|
| `hooks/prompts/state-update-reminder.md` | remove Section 5 "Content History Log" (`:67-86`) — the sole writer | SC1 |
|
||||||
|
| `config/content-history.template.md` | **DELETE** | SC1 |
|
||||||
|
| `hooks/scripts/migrate-data.mjs` | remove the B1 `MOVE_FILES` entry (`:33`) | SC1 |
|
||||||
|
| `hooks/scripts/__tests__/migrate-data.test.mjs` | drop content-history fixture (`:37`), its 2 assertions (`:56`,`:61`), trim test-(a) description (`:50`); cases a–e stay (count unchanged) | SC2 |
|
||||||
|
| `.gitignore` | remove `assets/analytics/content-history.md` (`:44`) | SC1 |
|
||||||
|
| `scripts/test-runner.sh` | narrow `SC2_CLASSES` (`:592`) — drop the `content-history\.md` alternative (check stays → gate count unchanged) | SC1/SC5 |
|
||||||
|
| `references/data-path-convention.md` | remove `content-history.md` from the data-tree diagram (`:26`) | SC1 |
|
||||||
|
| `hooks/scripts/session-start.mjs` | drop `content-history` from the comment list (`:94`) | SC1 |
|
||||||
|
|
||||||
|
### (c) Reconciliation (read-side)
|
||||||
|
|
||||||
|
| File | Change | SC |
|
||||||
|
|------|--------|-----|
|
||||||
|
| `scripts/brain/tests/reconcile.test.ts` | **NEW** — RED-first: coverage tiers + truncated-hook degradation + empty-state | SC3 |
|
||||||
|
| `scripts/brain/src/reconcile.ts` | **NEW** — pure `reconcileRecentPosts` + `parseRecentPosts` + `loadRecentPosts` IO seam | SC3/SC4 |
|
||||||
|
| `scripts/brain/src/cli.ts` | add `reconcile` subcommand (`runReconcile`) + dispatch (`:312`) + usage line | SC4 |
|
||||||
|
| `scripts/test-runner.sh` | **NEW Section 16f** (2 unconditional checks) + `ASSERT_BASELINE_FLOOR` 82→**84** + `BRAIN_TESTS_FLOOR` 114→**recount** + header enumeration | SC4/SC5 |
|
||||||
|
| `docs/second-brain/architecture.md` | mark the SB-S3 build-row **S3e ✅** (arc complete) | SC7 |
|
||||||
|
|
||||||
|
**Not touched (scope fence):** `hooks/scripts/state-updater.mjs`, `## Recent Posts`, `pruneContentHistory` (S3e **reads** silo 1, never writes) · `assemble.ts` join heuristic (reconcile *consumes* the graph) · `consolidate.ts`/`ingest.ts` (no engine change) · `consolidation-loop.md` · CLAUDE.md command/agent tables (no new command/agent; `brain reconcile` is an internal CLI subcommand) · README `:120` conceptual "content-history alignment" (concept, not the file — verify-then-leave).
|
||||||
|
|
||||||
|
## Step 1 — (RED) Reconcile test against the absent module
|
||||||
|
|
||||||
|
`scripts/brain/tests/reconcile.test.ts` — model the IO/pure split on `assemble.test.ts`. The RED is a **logic-RED, not an import-RED**: land a **stub `reconcile.ts`** first that compiles + exports the symbols but mis-classifies (returns `[]` / a wrong tier), so the named tier assertions go red against *logic*, not a `MODULE_NOT_FOUND`. Cases:
|
||||||
|
|
||||||
|
1. **`parseRecentPosts`** extracts `{date, hook, charCount, topic}` from a `## Recent Posts` block whose entries match the **writer's** exact format (`updatePostTracking`, `state-updater.mjs:116`: `- [YYYY-MM-DD] "hook" (chars) - topic`) — the format source of truth, NOT the date-only pruner regex (`:145`). A **golden-string** case feeds a literal writer-produced entry and asserts the round-trip. A non-`## Recent Posts` doc → `[]`; a `$`-bearing topic/hook round-trips verbatim (capture/`match` on read — no `String.replace` → no regex injection).
|
||||||
|
2. **`reconcileRecentPosts({recentPosts, records, graph})`** (the pinned 3-arg signature — `records` carry `body`, which `PostGraphNode` lacks) classifies each silo-1 entry:
|
||||||
|
- **in-graph** — a published record matches (date + hook-prefix on `record.body`) AND its graph node has an analytics match → the full chain is observable.
|
||||||
|
- **in-brain-only** — record matches but its graph node's analytics `confidence==='none'`.
|
||||||
|
- **orphaned-in-state** — no record matches (created via the plugin, never `brain ingest`-ed) → the coverage gap.
|
||||||
|
- **summary** counts (created N · in-graph M · orphaned K).
|
||||||
|
3. **Truncated-hook degradation** — a ≤60-char preview ending `...` strips the marker before prefix-matching (mirror `assemble.ts: stripTrailingEllipsis`); a preview too short to discriminate (below a `PREFIX_FLOOR`-style guard) → `orphaned`, never a false `in-graph`.
|
||||||
|
4. **Empty-state** — empty/absent `## Recent Posts` → empty report, no throw.
|
||||||
|
|
||||||
|
**RED proof (recorded — logic-RED):** with the mis-classifying stub in place, `(cd scripts/brain && npm test)` → the **named tier assertions** fail (e.g. "expected `orphaned-in-state`, got …"). Capture *that* transcript as the iron-law artifact in STATE/changelog — not a bare import error. **Circuit-breaker:** if the RED run is not recorded before the real `reconcile.ts` logic lands, halt and redo Step 1 (the RED proof is the only circuit breaker in this plan).
|
||||||
|
|
||||||
|
## Step 2 — (GREEN) `reconcile.ts` — pure core + parser + IO seam
|
||||||
|
|
||||||
|
`scripts/brain/src/reconcile.ts`:
|
||||||
|
|
||||||
|
- **`parseRecentPosts(stateText: string): RecentPost[]`** — regex the `## Recent Posts` section against the **writer's** format (`updatePostTracking`, `state-updater.mjs:116`: `- [${postDate}] "${hookPreview}" (${charCount}) - ${postTopic}`) → e.g. `/^- \[(\d{4}-\d{2}-\d{2})\] "(.*)" \((\d+)\) - (.+)$/gm`. **NB:** this is NOT `pruneContentHistory`'s regex — that one (`:145`) is **date-only** (`/^- \[(\d{4}-\d{2}-\d{2})\] .+$/gm`) and captures no hook/chars/topic; `parseRecentPosts` is the *first* full-field reader of this section and tracks the writer, not the pruner. Pure.
|
||||||
|
- **`reconcileRecentPosts({recentPosts, records, graph}): ReconcileNode[]` + `summarize(...)`** — pure, no FS/clock. `records` is the **full `PublishedRecord[]`** (carry `body`); `graph` is `PostGraphNode[]` (carry `contentId` + analytics `match`, NO `body`). Reuse `assemble.ts`'s `normalize` / `stripTrailingEllipsis` idiom (brain-local copy, NOT a new shared util — the repo's accepted duplication, `dataRoot.ts` header). Match `recentPost.hook` (ellipsis-stripped, normalized) as a **prefix of** `record.body`; confidence by date equality; below the prefix floor → `orphaned`. Then look up the matched `record.id` in `graph` to read its analytics tier (`in-graph` if the node's `match.confidence !== 'none'`, else `in-brain-only`).
|
||||||
|
- **`loadRecentPosts(): RecentPost[]`** — the new cross-seam IO. Resolve the state file with the **canonical precedence already exported as `data-root.mjs: getStateFile()`** — `process.env.STATE_FILE || join(resolveHome(), ".claude", "linkedin-studio.local.md")` where `resolveHome() = process.env.HOME || process.env.USERPROFILE || homedir()` (`data-root.mjs:17-19`). **NOT bare `homedir()`** — that silently ignores a `HOME`/`USERPROFILE` override the writer honours (`state-updater.mjs:12`); and **NOT `dataRoot()`** (that is the *dir* under `LINKEDIN_STUDIO_DATA`). The brain TS copies `getStateFile()`'s chain because it cannot import the `.mjs`. Read read-only; absent → `[]` (fresh-clone safe). Header caveat: this is a **new** root-skew seam (state file via `STATE_FILE`, a different root than the brain dataRoot) — not a mirror of `assemble.ts`'s `ANALYTICS_ROOT` note.
|
||||||
|
|
||||||
|
**GREEN gate (brain):** `(cd scripts/brain && npm test)` → reconcile cases pass; record the new live `tests` total → `BRAIN_TESTS_FLOOR` 114→**that number** (recorded, not guessed; update the inline breakdown comment `test-runner.sh:718` with "+ SB-S3e N [reconcile]").
|
||||||
|
|
||||||
|
## Step 3 — (GREEN) Wire `brain reconcile` into the CLI
|
||||||
|
|
||||||
|
`scripts/brain/src/cli.ts`:
|
||||||
|
- Add `runReconcile()` (model on `runAssemble`, `:161-191`). **Loader (BLOCKER fix):** do NOT use `listPublished()` — it takes no args and returns body-less `PublishedSummary[]` (`ingest.ts:262-289`). Instead reuse `runAssemble`'s inline loader (`cli.ts:163-174`): `readdirSync(pubDir).filter(.md).map(parsePublishedRecord)` → real `PublishedRecord[]` (with `body`/`specifics`/`trends`). Then `const analytics = loadAnalyticsRows()`, `const graph = assemblePostGraph({records, analytics})`, `const recentPosts = loadRecentPosts()`, and **call the core by its literal name** `reconcileRecentPosts({recentPosts, records, graph})` (no alias/re-export wrapper — Section 16f Check B greps `cli.ts` for the literal `reconcileRecentPosts`). Print per-post lines (`· <date> · <tier> · "<hook>"`) + summary + nudge ("K created posts are not in the brain graph — `brain ingest --file <p>` to feed them"). Read-only.
|
||||||
|
- Dispatch: insert `if (command === "reconcile") return runReconcile(flags);` **after** the `assemble` dispatch line (`:312`, i.e. at the new `:313` before `usage(...)`).
|
||||||
|
- Usage: add `" reconcile"` to the usage block **after** the `" assemble"` line (`:94`).
|
||||||
|
|
||||||
|
**GREEN gate (lint):** Section 16f Check B now passes (see Step 5).
|
||||||
|
|
||||||
|
## Step 4 — (b) Retire `content-history.md` (deletions)
|
||||||
|
|
||||||
|
Apply the eight edits in the (b) table. Order: remove the **writer** (`state-update-reminder.md` Section 5) first, then template, migration entry, gitignore, gate guard, ref-doc, comment. Then trim `migrate-data.test.mjs` — drop the content-history fixture write + the two assertions + the words "content-history moved" from the test-(a) name; cases a–e remain.
|
||||||
|
|
||||||
|
**Gate (hook suite — separate runner), recorded:** `node --test hooks/scripts/__tests__/migrate-data.test.mjs` → cases a–e green (capture the transcript — R8 is demonstrated, not asserted). Confirm cases b–e never expected `analytics/content-history.md` as a present path (they don't — only the deleted case-(a) fixture/assertions referenced it; b–e exercise MOVE_DIRS / idempotency / collision / empty), so removing the B1 MOVE entry cannot regress them. **SC1:** `grep -rn "content-history" hooks/ config/ scripts/test-runner.sh .gitignore references/data-path-convention.md` → zero (the README `:120` *concept* mention is out of this grep set; confirm it is the concept, leave it).
|
||||||
|
|
||||||
|
## Step 5 — (RED→GREEN) Gate Section 16f + floors + SC2 narrow
|
||||||
|
|
||||||
|
Insert **Section 16f** after Section 16e (`test-runner.sh:885`, before Section 17 `:886`), modelled on 16e's two-check idiom (self-test + real-file grep, `grep -qF` fixed-string):
|
||||||
|
|
||||||
|
- **Literals:** `RECON_CLI_LIT='command === "reconcile"'`, `RECON_CORE_LIT='reconcileRecentPosts'`.
|
||||||
|
- **`reconcile_wired()`** — wired iff a probe carries BOTH literals (echo twice; `grep -qF` — both literals contain regex-special chars, so `-F` is mandatory).
|
||||||
|
- **Check A (self-test, unconditional, non-vacuous):** a fully-wired probe MUST be detected; the rejected probes must each miss **exactly one** literal (the load-bearing discriminators), plus a specificity decoy:
|
||||||
|
- **D1 (discriminator):** dispatch present, core call absent — `command === "reconcile"` but no `reconcileRecentPosts` → must REJECT (proves the core literal is load-bearing).
|
||||||
|
- **D2 (discriminator):** core call present, dispatch absent — `reconcileRecentPosts(...)` but no `command === "reconcile"` → must REJECT (proves the dispatch literal is load-bearing).
|
||||||
|
- **D3 (specificity):** a sibling command fully wired to the WRONG domain — `command === "assemble"` + `assemblePostGraph` (neither reconcile literal) → must REJECT (proves the predicate is reconcile-specific, not "any command + any core").
|
||||||
|
→ `pass`/`fail` "reconcile self-test: full wiring detected; D1/D2 single-literal forms + D3 `assemble` specificity decoy rejected".
|
||||||
|
- **Check B (real-file grep, unconditional):** `grep -qF "$RECON_CLI_LIT" scripts/brain/src/cli.ts && grep -qF "$RECON_CORE_LIT" scripts/brain/src/cli.ts` → `pass` "brain CLI wired to reconcile (dispatch + core call by literal name)" else `fail`. Both literals live in `cli.ts` (the dispatch `if` + `runReconcile`'s literal-name call to the core — Step 3 pins "no alias"), so one file carries both → deps-free, like 16e Check B.
|
||||||
|
- **Header enumeration (`:33-39`):** insert the 16f clause **between** the 16e clause's terminal "…in Section 16e;" (`:38`) and the "the assertion-count anti-erosion floor (SC6) in Section 18." tail — preserving 16e → 16f → 18 ordering. The range "Sections 8–18" is unchanged (16f is in range).
|
||||||
|
- **`ASSERT_BASELINE_FLOOR` (`:966`):** 82 → **84** (+2 deps-free unconditional checks — they lift the deps-absent minimum, NOT pinned to the deps-present TOTAL); extend the history comment ("+2 for SB-S3e's two Section-16f checks (reconcile self-test + brain-CLI reconcile grep) = 84").
|
||||||
|
- **`SC2_CLASSES` (`:592`):** delete the `assets/analytics/content-history\.md|` alternative (now retired). The SC2 check is still one pass/fail → gate TOTAL change comes only from 16f's +2.
|
||||||
|
|
||||||
|
**RED proof:** between Steps 1 and 3, a gate run shows Section 16f Check A passing (self-test is self-contained) and **Check B FAILING** (cli.ts not yet wired); after Step 3 both pass.
|
||||||
|
|
||||||
|
**GREEN gate (full):** `bash scripts/test-runner.sh` → FAIL=0; TOTAL recounted **live**. **Recorded pre-change baseline (verified live this session, 2026-06-23): 97/0/0** → expected **99** after 16f's +2 (recount at land, never pin); ASSERT floor 84 honoured; SC2-dry-run green with the narrowed regex.
|
||||||
|
|
||||||
|
## Step 6 — Doc reconciliation
|
||||||
|
|
||||||
|
Edit the `architecture.md` SB-S3 build-row: `S3e ✅` — "dead `content-history.md` retired; `brain reconcile` reconciles post-tracking ↔ published ↔ analytics (read-side; auto-capture = follow-up)" — and note the arc is complete (S4 EØS-connector optional). No other architecture edit.
|
||||||
|
|
||||||
|
**CLAUDE.md surface (open-Q4, resolved in-plan):** `brain reconcile` is an **internal brain-CLI subcommand**, the same class as `brain assemble`/`ingest`/`consolidate` — none of which appear in CLAUDE.md's command/agent tables (those count *plugin* commands/agents). So **no CLAUDE.md count change**; the `brain reconcile` mention lives in the `architecture.md` build-row above. Confirm at the gate, but the default is no-plugin-surface.
|
||||||
|
|
||||||
|
## Step 7 — Behavioural verification (manual, recorded — SC4/SC6)
|
||||||
|
|
||||||
|
Not unit-testable (operator-accepted command-testing gap). Honest procedure, result recorded at land:
|
||||||
|
- **SC4 (e2e):** seed a temp root — a `## Recent Posts` block (via `STATE_FILE`), 1–2 `ingest/published` records (one matching a Recent-Posts entry, one not), an analytics batch — run `brain reconcile`; confirm it prints in-graph / orphaned tiers + the summary + nudge.
|
||||||
|
- **SC6 (root-skew):** with `STATE_FILE=<tmp>` it reads silo 1 from the temp file; unset → falls back to `$HOME/.claude/linkedin-studio.local.md`. **Honesty hedge (verifiseringsplikt):** if the per-call seam can't be exercised, record wiring-inspected-only, NOT a behavioural pass.
|
||||||
|
|
||||||
|
## Step 8 — Land
|
||||||
|
|
||||||
|
STATE "Telling" + "👉 NESTE" updated (S3e done → arc complete; next = strategic checkpoint / product-maturity). Commit. Mixed: reconcile + retirement + gate are code (`[skip-docs]`); brief+plan+architecture are docs. **Push only inside the window** (`date '+%u %H:%M'` first; `origin` = PUBLIC `open/` → confirm before push). No version bump (additive within v0.5.2 dev; release separate).
|
||||||
|
|
||||||
|
## Verification (testable)
|
||||||
|
|
||||||
|
| SC | Check | Command | Expected |
|
||||||
|
|----|-------|---------|----------|
|
||||||
|
| SC1 | content-history retired | `grep -rn content-history hooks/ config/ scripts/test-runner.sh .gitignore references/` + `ls config/content-history.template.md` | zero hits; file gone |
|
||||||
|
| SC2 | migration green | `node --test hooks/scripts/__tests__/migrate-data.test.mjs` | cases a–e pass |
|
||||||
|
| SC3 | reconcile core (RED→GREEN) | `(cd scripts/brain && npm test)` | reconcile cases pass; floor recounted |
|
||||||
|
| SC4 | reconcile wired | `bash scripts/test-runner.sh` + manual `brain reconcile` | Section 16f Check B passes; report prints |
|
||||||
|
| SC5 | gate green + floor | same run | 99/0/0 (confirm live); ASSERT floor 84; SC2-dry-run green |
|
||||||
|
| (red proof) | failing-test-first | gate + brain run BETWEEN Steps 1 and 3 | reconcile test fails; 16f Check B fails |
|
||||||
|
| SC6 | root-skew | manual, `STATE_FILE` set/unset | reads the pointed file / falls back (or honest fallback recorded) |
|
||||||
|
| SC7 | counts reconciled | inspect | architecture S3e ✅; STATE telling updated; no CLAUDE.md command/agent count change |
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **R1 — vacuous gate.** Mitigated by the 16e-style non-vacuity self-test with the `assemble` sibling-command decoy.
|
||||||
|
- **R2 — seam-mismatch (the load-bearing one).** `loadRecentPosts()` MUST resolve the state FILE via `getStateFile()`'s exact chain — `STATE_FILE || join(resolveHome(), '.claude', 'linkedin-studio.local.md')`, `resolveHome() = HOME||USERPROFILE||homedir()` (`data-root.mjs:17-19,30-31`) — NOT bare `homedir()` (ignores a HOME/USERPROFILE override) and NOT `dataRoot()` (the DIR under `LINKEDIN_STUDIO_DATA`). A divergence silently reads the wrong path. Mitigated: chain copied from the canonical `getStateFile()` (not `state-updater.mjs:12`, which falls through to `''` — a latent bug `data-root.mjs:14-16` warns against); SC6 exercises both `STATE_FILE` branches.
|
||||||
|
- **R3 — parse drift.** The reconcile entry regex must track the **writer** `updatePostTracking` (`state-updater.mjs:116`); if the writer's format changes, the reader silently misses. `pruneContentHistory` (`:145`) is date-only and would NOT catch such drift, so it is not a co-mover. Mitigated: a **golden-string test** feeds a literal writer-format entry through `parseRecentPosts` and asserts the round-trip — pinning the exact write format as the contract.
|
||||||
|
- **R4 — retirement over-reach.** Deleting a *conceptual* "content history" mention (README `:120`, or `pruneContentHistory`'s name) would break live behaviour. Mitigated: the (b) table is file-plumbing-only; the scope fence + SC1's bounded grep set exclude the concept; `pruneContentHistory` (operates on `## Recent Posts`) is explicitly out.
|
||||||
|
- **R5 — `$`-injection on READ.** `parseRecentPosts` uses `match`/capture (read), never `String.replace` with an untrusted replacement — so a `$`-bearing hook/topic round-trips verbatim (Step 1 case 1 pins it). No write path → no Section-13 `state-updater` surface touched.
|
||||||
|
- **R6 — floor false-fail on fresh clone.** 16f's two checks are deps-free (+2 → 84, correct). `BRAIN_TESTS_FLOOR` only checked when the brain suite runs (deps-present); a fresh clone warn-skips it.
|
||||||
|
- **R7 — honest-limit overclaim.** The brief/CLI must state plainly that read-side reports the coverage gap but cannot reconstruct un-captured specifics/trends (auto-capture = follow-up). Mitigated: §3 honest-limits + Step 7 verifiseringsplikt hedge.
|
||||||
|
- **R8 — migrate idempotency.** Removing the B1 MOVE entry must not break the `.migrated` marker / COPY-MOVE classes. Mitigated: cases b–e (idempotent re-run, collision, empty) stay green in Step 4's run.
|
||||||
|
|
||||||
|
## Plan-critic — folded
|
||||||
|
|
||||||
|
`voyage:plan-critic` (3 blockers + 6 major + 4 minor) — each verified directly against the code before folding:
|
||||||
|
|
||||||
|
- **B1 — body-less loader (load-bearing):** `listPublished()` takes no args and returns `PublishedSummary` (`{id,provenance,published_date,firstLine}`, `ingest.ts:262-289`) — NO `body`. The reconcile prefix-match AND `assemblePostGraph` both need `PublishedRecord.body`. **Folded Step 3:** use `runAssemble`'s inline `readdirSync().map(parsePublishedRecord)` loader (`cli.ts:163-174`), which yields real `PublishedRecord[]` (and already builds a `bodyById` Map). ✅
|
||||||
|
- **B2 — core-signature contradiction:** brief said `{recentPosts, graph}`; `PostGraphNode` has no `body` → graph-only can't prefix-match. **Folded** to the 3-arg `{recentPosts, records, graph}` in brief §3(c).1 + plan Steps 1/2 — one signature, agreed across brief/test/core. ✅
|
||||||
|
- **B3 — regex misattribution:** plan cited `pruneContentHistory`'s `:145` regex as full-field; it is **date-only**. **Folded Step 2 + R3:** the parser tracks the **writer** `:116`; `parseRecentPosts` is the first full-field reader; a golden-string test pins the write format. ✅
|
||||||
|
- **M4 — HOME seam:** `loadRecentPosts` must use `getStateFile()`'s `resolveHome()` chain (`HOME||USERPROFILE||homedir()`), not bare `homedir()` (ignores an override the writer honours). **Folded Step 2 + R2**, citing `data-root.mjs:17-19,30-31` as canonical (not `state-updater.mjs:12`, which falls through to `''`). ✅
|
||||||
|
- **M5 — weak gate decoys:** the both-absent `assemble` decoy was trivial. **Folded Step 5:** D1/D2 now miss **exactly one** literal each (the load-bearing discriminators); D3 (`assemble` sibling) is reframed as a specificity decoy. ✅
|
||||||
|
- **M6 — Check-B literal name:** `reconcileRecentPosts` appears in `cli.ts` only if `runReconcile` calls the core by its literal name. **Folded Step 3** ("no alias/wrapper") + Step 5 Check-B wording. ✅
|
||||||
|
- **M7 — unrecorded TOTAL:** **Folded Step 5** — recorded the live pre-change gate **97/0/0 (2026-06-23)**; 99 is the expected post-16f count, recount at land. ✅
|
||||||
|
- **M8 — migrate idempotency asserted not shown:** **Folded Step 4** — require the recorded `node --test migrate-data.test.mjs` green run; confirm cases b–e never expected content-history present. ✅
|
||||||
|
- **M9 — SC1 grep divergence:** the brief's `--include`-filtered grep would skip `.gitignore`/`.md`. **Folded** — brief SC1 unified with the plan's unfiltered explicit-list grep. ✅
|
||||||
|
- **m10 — header insertion point:** **Folded Step 5** — insert the 16f clause between the 16e clause terminal and the Section-18 tail. ✅
|
||||||
|
- **m11 — `:312`/`:94` line refs:** **Folded Step 3** — "after :312" / "after :94" (insertion, not the existing line). ✅
|
||||||
|
- **m12 — CLAUDE.md deferred:** **Folded Step 6** — resolved in-plan: `brain reconcile` is internal brain-CLI (like `assemble`), no CLAUDE.md count change. ✅
|
||||||
|
- **m13 — circuit-breaker:** **Folded Step 1** — "if RED not recorded before reconcile.ts logic lands, halt and redo Step 1." ✅
|
||||||
|
|
||||||
|
**`voyage:scope-guardian`: ALIGNED** — 0 creep / 0 gaps / 0 dependency issues. All 14 IN items map to a step; no NON-GOAL file touched; scope fence consistent (slightly broader/defensive); doc reconciliation proportional. Every file/line/symbol verified live.
|
||||||
|
|
||||||
|
Cross-checks confirmed sound: the (b) retirement arm was already nearly executable (all 8 surfaces verified); floor arithmetic (ASSERT 82→84 from 16f's +2; SC2-narrow nets 0; BRAIN 114→recount); the "read-side does not close the gap" honesty (auto-capture = the real closer, follow-up) stands; `$`-injection is absent on the read path (capture/`match`, never `String.replace`).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue