feat(linkedin-studio): SB-S1 wire voice-trainer to published-only + gate-enforce it [skip-docs]

voice-trainer's Gather step now reads ingest/published/ (provenance=published) as
the primary gold source, keeps voice-samples as a human tributary (not reshaped),
forbids learning from provenance=ai-draft, and fences the auto-append trap. New gate
Section 16c (Brain Published-Only Invariant) enforces the wiring with exact-literal
greps (grep -F 'ingest/published' + 'provenance=ai-draft') + a non-vacuity self-test
(rejects an 'AI-generated'-only probe), against voice-trainer.md + the new contract
doc docs/second-brain/ingest-manual-import.md. BRAIN_TESTS_FLOOR 34→63; assertion
floor 75→78 (+3 unconditional checks; not pinned to deps-present TOTAL to preserve the
warn-skip margin). Gate 90→93/0/0.

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 15:02:32 +02:00
commit ac532f9218
3 changed files with 155 additions and 4 deletions

View file

@ -133,7 +133,7 @@ Architecture: [prose/sectioned/framework]
### Analysis Process ### Analysis Process
1. **Gather** — Read all files in `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`, existing profile from `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md`, and template from `config/user-profile.template.md` 1. **Gather (published-only gold signal first)** — Read the user's actual published posts from `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/ingest/published/` (`provenance=published` — the highest-trust source). Then read the human-curated `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/` corpus (kept as a tributary, not reshaped), the existing profile from `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md`, and the template from `config/user-profile.template.md`. **Never learn voice patterns from any content marked `provenance=ai-draft`** — a content engine that learns its own voice from its own drafts collapses toward its priors (this generalises voice-scrubber's "gold standard = approved editions, never the draft corpus"). **Fence:** any future auto-capture into a learning corpus (the voice-samples template's Stop-hook hint) MUST tag provenance and admit `provenance=published` only — never blind-append generated drafts.
2. **Analyze** — Apply all six dimensions to each sample. Note dates for temporal analysis. Flag inconsistent samples as outliers or evolution. 2. **Analyze** — Apply all six dimensions to each sample. Note dates for temporal analysis. Flag inconsistent samples as outliers or evolution.
3. **Synthesize** — Patterns in 70%+ of samples = core traits. 40-70% = situational traits (note context). <40% = experimental traits. Track temporal trends. 3. **Synthesize** — Patterns in 70%+ of samples = core traits. 40-70% = situational traits (note context). <40% = experimental traits. Track temporal trends.
4. **Build** — Compile into Voice Profile Document format. Include confidence levels (high/medium/low) and concrete examples for every trait. 4. **Build** — Compile into Voice Profile Document format. Include confidence levels (high/medium/low) and concrete examples for every trait.
@ -141,12 +141,15 @@ Architecture: [prose/sectioned/framework]
### Sample Quality Priorities ### Sample Quality Priorities
0. **`ingest/published/` records (`provenance=published`) — the gold signal; rank above all else.**
1. Published posts with high engagement (audience-validated authenticity) 1. Published posts with high engagement (audience-validated authenticity)
2. Recent samples (last 6 months reflect current voice) 2. Recent samples (last 6 months reflect current voice)
3. Author-confirmed samples ("this sounds like me") 3. Author-confirmed samples ("this sounds like me")
4. Longer samples (more data points) 4. Longer samples (more data points)
5. Varied contexts (different content types reveal range) 5. Varied contexts (different content types reveal range)
**Exclude unconditionally: anything marked `provenance=ai-draft`** (the model-collapse guard — never train voice on the engine's own drafts).
Flag if: fewer than 5 samples (low confidence), single time period (temporal bias), or contradictory patterns (possible ghostwriting). Flag if: fewer than 5 samples (low confidence), single time period (temporal bias), or contradictory patterns (possible ghostwriting).
## Voice Drift Detection ## Voice Drift Detection

View file

@ -0,0 +1,87 @@
# Manual-import contract — ingest + the published gold signal (SB-S1)
> The contract for getting your **actual published posts** into the second brain as the
> **gold signal** (`provenance=published`) that voice/profile learning is allowed to learn from.
> Part of the second-brain arc (`architecture.md`); landed in SB-S1.
## Why this exists
Profile and voice must learn from **human-published content only** — never from what the engine
itself drafted. A content engine that learns its own voice from its own drafts collapses toward its
own priors (model collapse). So the brain has one provenance vocabulary — `human | published | ai-draft`
— and the learning surfaces are bound to `published` and **forbidden from `provenance=ai-draft`**.
A published post already lands in three non-referencing places today (the state file's `## Recent Posts`,
the dead `analytics/content-history.md`, and `analytics/posts/*.json` from the CSV) — but **none of them
holds the full post text**. Voice learning needs the verbatim text, so `ingest/published/` storing it is
additive, not a re-count. (Threading a shared id across those silos is a later slice, SB-S3.)
## The drop-zone model
```
${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/ingest/
inbox/ # drop-zone: paste a published post here as a .md file (manual, or a future connector)
published/ # processed gold records, provenance=published — one file per post, <id>.md
```
- **`inbox/`** — you drop raw post files here (top-level `*.md`, one post per file).
- **`published/`** — the `ingest` CLI writes processed, provenance-tagged records here. `<id>.md` where
`<id> = sha256(verbatim body)[:12]` — byte-identity, so the same post never duplicates and two
genuinely different posts never collide.
## CLI usage
Run from `scripts/brain` (`node --import tsx src/cli.ts <cmd>`), or via the package's `start` script.
```bash
# one post from a file → ingest/published/<id>.md, tagged provenance=published
brain ingest --file path/to/post.md [--source manual] [--date 2026-05-26]
# process everything dropped in ingest/inbox/ (top-level *.md, skips dotfiles), non-destructive
brain ingest --scan-inbox
# inspect the gold corpus — id · provenance · date · first line (eyeball that nothing ai-draft leaked)
brain published list [--json]
```
- `--date` is the real publish date; it defaults to the ingest date when omitted.
- Re-ingesting the same body is a **no-op** (idempotent). An id-collision with a *differing* body is
disambiguated to `<id>-2.md` — a differing gold record is **never silently dropped**.
- `ingest` **creates `ingest/published/` on demand** — it does not require a prior `brain init`.
## Record file grammar (no YAML)
One file per post: a fixed five-line header, a `---` sentinel, then the **verbatim body**.
```
id: 1a2b3c4d5e6f
provenance: published
published_date: 2026-05-26
captured_at: 2026-06-23
source: manual
---
The verbatim post body goes here.
It may contain anything — blank lines, ] | characters, even a line that looks
like --- — because the parser splits on the FIRST sentinel only.
```
`parse ∘ serialize` is an exact identity over all six fields including the body. A record in
`published/` whose `provenance` is anything other than `published` is treated as a **corruption signal**
and rejected on parse.
## The published-only learning rule (and its honest limit)
- `agents/voice-trainer.md` is wired to read `ingest/published/` as its primary gold source and to
**exclude `provenance=ai-draft` unconditionally**. This wiring is **gate-enforced** by a structure-lint
(`scripts/test-runner.sh`, the Brain Published-Only Invariant section): the lint asserts the agent
carries both the `ingest/published` source and the `provenance=ai-draft` exclusion.
- **Honest limit:** the *wiring* is gate-enforced, but an agent instruction can still be ignored at
runtime — the gate proves the rule is present, not that the model obeyed it on a given run. The
strongest enforcement short of executing the agent.
## What SB-S1 does NOT do
No consolidation loop and **no automatic `brain/profile.md` mutation** (SB-S2 owns the sleep-time
profile-diff loop) · no session-start wiring · no cross-silo id threading (SB-S3) · no connector
(SB-S4). `source` carries a forward-compatible field for a future connector, but no connector code ships.

View file

@ -711,7 +711,7 @@ if [ -x "$BR_DIR/node_modules/.bin/tsx" ]; then
BR_OUT=$( set +e; (cd "$BR_DIR" && npm test) 2>&1; echo "BR_EXIT:$?" ) BR_OUT=$( set +e; (cd "$BR_DIR" && npm test) 2>&1; echo "BR_EXIT:$?" )
BR_EXIT=$(echo "$BR_OUT" | grep -oE 'BR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1) BR_EXIT=$(echo "$BR_OUT" | grep -oE 'BR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
BR_TESTS=$(echo "$BR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1) BR_TESTS=$(echo "$BR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
BRAIN_TESTS_FLOOR=34 # SB-S0: id(11) + profile(6) + fold(12) + scaffold(5) BRAIN_TESTS_FLOOR=63 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)]
if [ "$BR_EXIT" = "0" ] && [ -n "$BR_TESTS" ] && [ "$BR_TESTS" -ge "$BRAIN_TESTS_FLOOR" ]; then if [ "$BR_EXIT" = "0" ] && [ -n "$BR_TESTS" ] && [ "$BR_TESTS" -ge "$BRAIN_TESTS_FLOOR" ]; then
pass "brain suite green: $BR_TESTS tests pass (floor $BRAIN_TESTS_FLOOR)" pass "brain suite green: $BR_TESTS tests pass (floor $BRAIN_TESTS_FLOOR)"
else else
@ -723,6 +723,62 @@ fi
echo "" echo ""
# --- Section 16c: Brain Published-Only Invariant (SB-S1) ---
echo "--- Brain Published-Only Invariant ---"
# SB-S1 wires voice-trainer to learn from the published gold signal ONLY: the agent
# must (a) name ingest/published/ as a source AND (b) carry the explicit exclusion of
# provenance=ai-draft (the model-collapse guard — never train voice on the engine's own
# drafts). Both literals are required. voice-trainer.md ALREADY contains "AI-generated"
# (a drift-cause description, line ~175), so a loose ai.?draft pattern would pass
# vacuously — we grep the EXACT literals with grep -F. The contract doc must also state
# the rule. Non-vacuity self-test mirrors Sections 8/13/17: a probe counts as "wired"
# iff it carries BOTH literals; probes missing either (incl. one with "AI-generated" but
# not "ai-draft") must NOT count as wired. The wiring is gate-enforced; the agent's
# runtime OBEDIENCE is not provable here (stated honestly in the contract doc).
PUB_LIT='ingest/published'
DRAFT_LIT='provenance=ai-draft'
pub_wired() { # $1 = text; wired iff BOTH literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$PUB_LIT" && echo "$1" | grep -qF "$DRAFT_LIT"
}
PUB_SELFTEST_OK=1
if ! pub_wired "reads from ingest/published/ and excludes provenance=ai-draft"; then
PUB_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if pub_wired "$probe"; then
PUB_SELFTEST_OK=0; echo " false-positive FAIL: under-wired probe accepted -> $probe"
fi
done <<'NEGATIVE16C'
reads from ingest/published/ but never states the draft exclusion
excludes provenance=ai-draft but never names the published source
flags AI-generated drift but carries neither required literal
NEGATIVE16C
if [ "$PUB_SELFTEST_OK" -eq 1 ]; then
pass "published-only self-test: full wiring detected; 3 under-wired forms rejected (incl. 'AI-generated'-only)"
else
fail "published-only self-test failed — the invariant lint is vacuous or over-eager"
fi
VT="agents/voice-trainer.md"
if grep -qF "$PUB_LIT" "$VT" && grep -qF "$DRAFT_LIT" "$VT"; then
pass "voice-trainer.md wired to published-only (names '$PUB_LIT', excludes '$DRAFT_LIT')"
else
fail "voice-trainer.md missing published-only wiring — needs both '$PUB_LIT' and '$DRAFT_LIT'"
fi
PUB_DOC="docs/second-brain/ingest-manual-import.md"
if grep -qF "$DRAFT_LIT" "$PUB_DOC"; then
pass "ingest-manual-import.md documents the published-only rule ('$DRAFT_LIT')"
else
fail "ingest-manual-import.md missing the published-only rule literal '$DRAFT_LIT'"
fi
echo ""
# --- Section 17: De-Niche Guard (B-S1 + B-S2) --- # --- Section 17: De-Niche Guard (B-S1 + B-S2) ---
echo "--- De-Niche Guard ---" echo "--- De-Niche Guard ---"
@ -794,8 +850,13 @@ echo ""
# The lint self-modifies its own checks, so a green run could mask a silently dropped # The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count # assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count
# may only grow (brief-reviewer assumption 3). History: 74 pre-M0; +1 for the SB-S0 # may only grow (brief-reviewer assumption 3). History: 74 pre-M0; +1 for the SB-S0
# brain-suite floor (Section 16b) = 75. Runs last so TOTAL_CHECKS sees every prior check. # brain-suite floor (Section 16b) = 75; +3 for SB-S1's three UNCONDITIONAL Section-16c
ASSERT_BASELINE_FLOOR=75 # checks (published-only self-test + voice-trainer grep + contract-doc grep) = 78.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop
# the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the
# deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh
# clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=78
TOTAL_CHECKS=$((PASS + FAIL)) TOTAL_CHECKS=$((PASS + FAIL))
if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then
pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR" pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"