Slice SB-S1 (Ingest + gold signal): manual import → ingest/published/ with provenance=published; wire voice-trainer to learn from published-only (the model-collapse guard). Brief + plan hardened through light-Voyage: brief-reviewer (PROCEED_WITH_RISKS), plan-critic (REVISE 63→folded), scope-guardian (GAP→folded). Key decisions: verbatim-body content hash (no normalizeContent, avoids silent data loss), one pinned record separator with a round-trip edge battery, gate-enforced published-only lint, S1/S2 boundary held (no profile.md mutation in S1). Operator-approved scope; version bump to 0.5.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
16 KiB
Task Brief — SB-S1 (Ingest + gold signal)
Status: DRAFT for adversarial brief-review (light-Voyage step 2). Not yet operator-approved. Arc:
architecture.md(approved 2026-06-23). Predecessor: SB-S0 landed (8c92719) —scripts/brain/(types/id/profile/scaffold/cli, 34 tests, gate-wired). This brief: the second slice in detail; S2–S4 stay one-line in the arc. Inputs:architecture.md§"evolution loop" steps 1–2, §"invariants" (provenance-weighted learning); SB-S0 code underscripts/brain/; the silo inventory (docs/second-brain/research/silo-inventory.md). Review delta (2026-06-23): folded the brief-review (PROCEED_WITH_RISKS). Resolver standardised ondataRoot(M-resolver); D3 voice-samples↔ingest relationship pinned (M-D3); SC5 hardened to a gate-checkable lint (M-SC5); test-runner floor edits named as deliverables (m-floor); input path + D4 net-new note + Q3 sharpened (minors).
1. Objective
Capture the gold signal — the user's actual published posts — into ingest/published/ tagged provenance=published, and wire the voice/profile-learning surface to learn from that published signal only, never from AI-drafted content. This is the one invariant the whole second-brain stands or falls on (the model-collapse guard): a content engine that learns its own voice from what it itself drafted collapses toward its own priors. SB-S1 lays the gold signal before the consolidation loop (SB-S2) that will consume it.
2. Background (why this slice, why now)
- The invariant already has a single-agent precedent.
voice-scrubberenforces "gold standard = approved Norwegian editions, never the English post corpus." SB-S1 generalises that one agent's rule into a system-wide, data-backed invariant: learning consumesprovenance=publishedonly. - The signal is genuinely new, not a re-count of existing silos. A published post lands today in three non-referencing places — state
## Recent Posts(hook + topic only), the deadanalytics/content-history.md, andanalytics/posts/*.json(metrics, from CSV). None holds the full post text. Voice learning needs the verbatim text;ingest/published/holding it is additive, not redundant. (Cross-silo threading of a shared id is SB-S3, explicitly out of scope here.) - The provenance shape is already in place. SB-S0 pinned
Provenance = human | published | ai-draftand put aprovenancefield on everyProfileFact. SB-S1 is the first slice to actually producepublishedrecords and gate a learning consumer on them — it spends the spine S0 built. - Manual-first is the contract. Auto-ingest (EU/EEA DMA portability) is a later additive tributary (SB-S4); SB-S1's ingest is the manual path: the user supplies the published post text, the CLI lands it as a gold record.
3. Confirmed decisions (inherited from the approved arc — not for re-litigation)
- Boundary: engine → plugin · user data → data dir · cockpit → Maskinrommet.
- Plain-text/JSON + git, no vector/graph DB. Claude is the retrieval engine.
- Provenance-weighted learning (
publishedonly, neverai-draft) is an invariant, not a toggle. - Tributaries keep their own schema/lifecycle — SB-S1 does NOT reshape voice-samples, specifics, trends, or analytics stores.
- No new seam function — ingest paths resolve through the brain package's own inlined
dataRoot(sub)resolver (scripts/brain/src/dataRoot.ts, the repo idiom — NOT the hooks-sidegetDataRoot()seam, a different function in a different package). So the twin-sync surface (data-root.mjs ⇄ storage.ts) stays untouched.
Review SHOULD challenge: the S1/S2 boundary (§5/§7), the store format (§5.1), feasibility, hidden coupling, testability, scope leak, missing risks. Review SHOULD NOT re-open the five decisions above unless internally contradictory or technically impossible.
4. Scope — where SB-S1 sits in the arc
| Slice | One-line scope | This brief |
|---|---|---|
| SB-S0 — Foundation | scaffold + profile fold + id/provenance spine | ✅ landed |
| SB-S1 — Ingest + gold signal | manual import → ingest/published/ provenance=published; wire the voice/profile-learning surface to published-only |
← this |
| SB-S2 — Evolution loop | sleep-time consolidation: journal+published+tributary deltas → profile diff (evidence/provenance/timestamps); threshold promotion; contradiction reconciliation; freshness nudge. Owns session-start wiring of init. | later |
| SB-S3 — Cross-silo graph + ops centre | thread the id through tributaries (post↔specific↔trend↔analytics); flesh out operations.md; retire dead content-history.md |
later |
| SB-S4 (optional) | EU/EEA DMA portability as auto-tributary into ingest/inbox/ |
later |
5. SB-S1 in detail (the build target)
5.1 Deliverables
-
A published-record data layer (TS,
scripts/brain/). APublishedRecordshape and pure parse/serialize functions for a published post file. Reuse the SB-S0 idioms:mintEntityIdfor the canonical id, the constrained-header line-grammar idea (NO YAML dep),normalizeProvenancefor the provenance token. Fields (proposed, open to the plan):id(sha256[:12] of normalized post text) ·provenance(alwayspublished) ·published_date(YYYY-MM-DD) ·captured_at(YYYY-MM-DD) ·source(manual| future connector) · the verbatim post body. Pure functions, deterministic, unit-tested. -
A
brain ingestCLI extendingscripts/brain/src/cli.ts(same dispatch idiom). Proposed subcommands (final shape open to the plan):brain ingest <file>— read a file containing one published post, mint id, writeingest/published/<id>.mdwithprovenance=published. Idempotent: same text → same id → no duplicate (compare-then-skip, matchinginitBrain).brain ingest --scan-inbox— process every file iningest/inbox/intoingest/published/, then mark/clear the inbox source (drop-zone → processed, per the architecture'sinbox→publishedmodel).brain published list— inspect the gold corpus (id ·provenance· date · first line). Surfacingprovenanceper record makes the invariant observable — the operator can eyeball that nothingai-draftleaked intopublished/.
-
Wire the voice/profile-learning surface to published-only (the invariant made enforceable). Edit
agents/voice-trainer.md's "Gather" step. The pinned relationship to the existing corpus (resolves brief-review M-D3):- ADD
ingest/published/(provenance=published) as the primary gold source, ranked above the legacyvoice-samples/corpus. - KEEP
voice-samples/as a human-curated tributary — not reshaped, not redirected, not abandoned (§7 honoured). By its existing contract its files are the user's own real writing (human-supplied via/setup//onboarding); they are notai-draft. voice-trainer keeps reading it but treatsingest/published/as the higher-trust signal. - FORBID learning voice/profile patterns from any content marked
provenance=ai-draft— the explicit negative rule that generalisesvoice-scrubber's "gold standard = approved editions, never the draft corpus" precedent to the system invariant. - FENCE the auto-append trap: the
voice-samples/template hints at a (not-yet-built) Stop-hook that auto-appends posts after content sessions. S1 states explicitly that any such future auto-capture MUST tag provenance and admitpublishedonly — never blind-append generated drafts into a learning corpus. (Forward-looking guard, no S1 code; names the exact model-collapse trap so a later slice cannot reintroduce it.) - voice-trainer stays Sonnet (no model/frontmatter change → no structure-lint model-consistency break).
- ADD
-
Manual-import contract doc —
docs/second-brain/ingest-manual-import.md: the published-record file format, the CLI usage, and the published-only learning rule. (Design doc →docs/, NOTreferences/, so the reference-doc count guard does not fire.) Net-new (verified): SB-S0 shipped theingest/{inbox,published}dirs only; no contract doc exists yet — the architecture's "SB-S0 … manual-import contract" wording referred to the dir shape, not a doc. -
Tests (TDD, failing-first) — published-record parse/serialize round-trip (incl. a body with
]/|/quotes/newlines), id determinism + dedup, CLI idempotency on re-ingest and--scan-inbox, source-absent / empty-inbox no-op. Plus the SC5 published-only structure-lint (a new gate check; see SC5). Gate stays green. -
Gate floor edits (named — Edit, not Write, on existing files → pathguard-safe): bump
BRAIN_TESTS_FLOORinscripts/test-runner.shto the new brain-suite count, and bump the assertion-count floor (ASSERT_BASELINE_FLOOR/ theTOTAL_CHECKSanti-erosion floor) by the number of newpass()lines added (the BRAIN suite line is unchanged-count; the new SC5 structure-lint check adds one). Exact line numbers confirmed in the plan against the livescripts/test-runner.sh. -
Docs — flip the
architecture.mdSB-S1 row to "landed"; this brief + the SB-S1 plan persisted underdocs/second-brain/.
5.2 The S1/S2 boundary (the decision the review must pin)
- SB-S1 ships: the gold-signal data layer (records + CLI), and wires the existing on-demand learning surface (
voice-trainer, invoked when the user runs a voice command) to the published-only rule. This is a data module + an agent-doc edit — no scheduler, no automatic profile mutation. - SB-S2 owns: the automated sleep-time consolidation loop (session-start cadence) that reads
published+ journal + tributary deltas and proposesprofile.mddiffs with evidence/provenance/timestamps + threshold promotion. No profile.md is auto-mutated in S1.
This boundary keeps S1 a clean, testable data+invariant slice and leaves all automatic profile evolution to S2. It is the #1 thing for the operator to confirm.
6. Success criteria (SB-S1 — testable)
- SC1 (ingest, runtime): with
LINKEDIN_STUDIO_DATAset to a temp dir,brain ingest <file>createsingest/published/<id>.mdwithprovenance=publishedand the verbatim body, at the runtime data-path via the brain package'sdataRoot.ingestcreatesingest/published/on demand (mkdir-recursive) — it does NOT hard-depend on a priorbrain init, so ingest-before-init works. Verified by a test asserting file existence + content with no prior init. - SC2 (record round-trip): a
PublishedRecordserializes and parses back to an identical record, including a body containing],|, quotes, and embedded newlines. Verified by a parse/serialize identity unit test. - SC3 (id determinism + dedup): the same post text mints the same id (and re-ingesting it is a no-op — no duplicate file); two different posts mint different ids. Verified by id + idempotency unit tests.
- SC4 (scan-inbox): files dropped in
ingest/inbox/are processed intoingest/published/by--scan-inbox; an empty inbox is a clean no-op; re-running does not re-create already-published records. Verified by a temp-dir test. - SC5 (published-only invariant, gate-checkable): a new structure-lint check in
scripts/test-runner.sh(matching the Section 8–17 honesty-lint idiom: positive + negative assertion + a non-vacuity self-test) asserts thatagents/voice-trainer.md(a) namesingest/published/in its gather/source instructions AND (b) carries the explicit negative string forbidding learning fromprovenance=ai-draft. The self-test proves the check is non-vacuous (it would FAIL on a fixture missing either string). The honest runtime limit (an agent instruction can still be ignored at runtime) is stated in the doc — but the presence of the wiring is gate-enforced, not checklist-trusted. (Resolves brief-review M-SC5: no "OR checklist" escape hatch.) - SC6 (no regression / no new seam):
scripts/test-runner.shgreen; floors hold (trends ≥24, specifics ≥28, contract ≥33, brain ≥ its new floor); hook tests pass; SB-S1 adds no new seam function sodata-root.test.mjstwin-sync is untouched.
7. Non-goals / scope fence (SB-S1 does NOT)
- No consolidation loop / no automatic profile mutation (SB-S2).
brain ingestwrites only underingest/, never tobrain/profile.md. - No session-start wiring of init or ingest (SB-S2 owns scaffold wiring; ingest stays a manual CLI in S1).
- No cross-silo id threading — ingest mints its own record id but does NOT weave it into trends/specifics/analytics/state (SB-S3).
- No reconciliation of the triple-recorded post and no retirement of
content-history.md(SB-S3). - No connector / no LinkedIn API (SB-S4).
sourceallows a future connector value but no connector code ships. - No tributary-schema edits — voice-samples, specifics, trends, analytics stores keep their current shape. (
voice-trainer.mdis edited; the voice-samples store format is not.) - No new seam function, no state-file / two-roots reconciliation, no GUI/cockpit.
8. Constraints / risks
- TDD iron law: no production code without a failing test first.
- Code placement: all new code is TS under
scripts/brain/— no new.mjsunderhooks/scripts/, so thepre-write-pathguard.mjsCategory-6 guard does not fire (it matches…/hooks/scripts/*.mjson Write only; SB-S1 writes none).agents/voice-trainer.mdis an existing file → Edit, not Write → unaffected regardless. - Public repo (
open/Forgejo): confirm before push; push only inside the window; STATE.md stays gitignored. - Structure lint: SB-S1 adds no
agents/,commands/,references/, orskills/file (ingest is a CLI subcommand in an existing package; voice-trainer is edited; the contract doc lives indocs/). So no count guard fires absent a version bump. Version bump is an open question (§9): the agent behavioural change may warrant 0.5.0 → 0.5.1. - Node v25 test quirk:
node --test <dir>fails — usenode --import tsx --test tests/*.test.ts; run TS tools from their own dir;npm installinscripts/brainbefore the gate or the suite warn-skips. - Risk — store-format ambiguity: the published-record file format is the one data-shaping decision in S1 (mirrors SB-S0's fold risk). Mitigate by pinning the grammar + a round-trip test (SC2) before the CLI wires it.
- Risk — invariant enforceability: "wire voice-trainer to published-only" is an agent-doc instruction, not executable code; an instruction can be ignored at runtime. Mitigate by making SC5 a gate-checkable lint where feasible (grep for the rule's presence), and by stating the limit honestly in the doc.
- Risk — S1/S2 scope creep: the temptation is to start consuming
published/intoprofile.mdhere. Fence held by §7 + SC (no test asserts profile mutation in S1).
9. Open questions for the operator (settle at "go")
- S1/S2 boundary (§5.2): confirm S1 = data layer + voice-trainer invariant wiring, S2 = the automatic profile-diff loop. (Recommended.)
- Profile vs voice in S1: confirm that profile learning is invariant-only in S1 (provenance already on facts; no profile mutation) and that the only active consumer wired is
voice-trainer. (Recommended.) - Store format (~decided, confirm): published records as file-per-post constrained-header markdown (the
brain/profile.mdline-grammar idiom, no YAML) — matching the architecture'sinbox→publishedper-file drop-zone. This deliberately does NOT reuse the specifics-bank single-JSON-index idiom (that store is a topic-tagged bank, not a per-file drop-zone). Confirm. - Version bump: take 0.5.0 → 0.5.1, or no bump? SB-S0 set a no-bump precedent for count-neutral slices, but SB-S1 differs: it changes a shipped agent's learning behaviour (
voice-trainer), a user-visible capability change — a stronger case for 0.5.1 than SB-S0 had. (Lean 0.5.1.)
10. Definition of done (SB-S1)
SC1–SC6 pass; deliverables 1–7 exist; non-goals untouched; the published-only invariant is wired into voice-trainer + documented + gate-enforced (SC5 structure-lint); the BRAIN + assertion floors raised; committed (push per window, confirm first); architecture.md SB-S1 row → "landed"; version/CLAUDE.md/README touched only if a bump is taken.