feat(linkedin-studio): N8 — stemme-kjeden (chronicle-voice-profile + drift-logg + language-resolve + auto-gull) [skip-docs]

Bind the long-form VOICE before prose — the discipline the line already
applies to SOURCES (validate-binding), its strongest link.

- C-1/A2-F1: voice-trainer distils a chronicle-voice-profile from the
  published gold corpus (per language/format); newsletter Step 0/3a/3b bind
  drafting to it, with an explicit never-silent fallback to the short-form
  corpus (which the engine itself calls degrading for chronicle voice).
- C-2: Step 3a Pass 0 READS the chronicle-voice-drift-log as a negative list
  (it was append-only, never consumed); Step 3b re-reads it in expansion.
- C-3: Step 1 resolves + persists the top-level edition-state.language, making
  the template doc-string's "Resolved at Step 1" claim true; the Norwegian
  review axis (language-reviewer / voice-scrubber) now fires when the edition
  IS Norwegian instead of being silently off by default. Step 6.5 already
  consumed the field — resolving it closes the axis end-to-end.
- A2-F2: Step 8 lock appends the locked edition to ingest/published
  (provenance=published), so the gold corpus the profile distils from grows
  one edition at a time — never the engine's drafts.

Domain-general throughout (languages/formats from the user's own corpus).
test-runner 140 -> 146 (Section 16o: 6 unconditional voice-chain greps +
non-vacuity self-test; anti-erosion floor 123 -> 129). All six suites green
(trends 300, brain 134, hooks 140, tests 35, render 60).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bE7VbkmR3cqHFEeGfzgwb
This commit is contained in:
Kjell Tore Guttormsen 2026-07-23 22:58:38 +02:00
commit b30b0e208f
4 changed files with 197 additions and 13 deletions

View file

@ -176,9 +176,24 @@ the edition left off before doing anything.
Step 2. Do not confuse `<serie>/STATE.md` (this edition's production state)
with the plugin's own `STATE.md` / `docs/BUILD-HANDOVER.local.md` (which govern
building the plugin itself).
4. **Read the voice profile**`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/authentic-voice-samples.md`
and anything else under `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`. Long-form must match the
author's voice; this is the reference for every drafting and review phase.
4. **Read the long-form voice profile** — the **chronicle-voice-profile**
(`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/chronicle-voice-profile.md`),
the distilled chronicle voice for THIS edition's language/format, built by
`voice-trainer` from the published gold corpus. This is the **primary reference
for every long-form drafting and review phase**. Short-form feed posts and
long-form editions are different registers of the same author, so long-form must
**not** be drafted against the English short-form corpus
(`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/authentic-voice-samples.md`) —
that corpus encodes rules (e.g. no em-dash) that are wrong for chronicle voice
and would degrade it. Read the `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`
corpus as a **secondary tributary** (register cues), never as the primary
long-form reference.
**Fallback (never silent).** If `chronicle-voice-profile.md` is absent — or has
no section for this edition's language/format — say so plainly, note the
chronicle voice is uncalibrated, and fall back to today's behavior (draft against
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`), while
flagging the gap so the operator can seed the gold corpus (Step 8 auto-gull grows
it from each locked edition).
5. **Resolve the active personas (per-artifact).** Personas are configured **per
edition**, not from one fixed global file. Resolve the set for
`articles.<currentArticle>` in this order (Step 1 finalizes + persists it):
@ -258,12 +273,12 @@ Then display a short status:
Edition: <series title> — article <currentArticle> "<title>"
Last completed phase: <currentPhase> (or: NEW edition)
Resuming at: Step <N> — <step name> (from the resumption table above)
Voice profile: loaded | MISSING
Chronicle voice profile: loaded (<language>/<format>) | MISSING → fallback to short-form corpus
Persona library: <N> personas loaded (active set chosen in Step 1)
```
If the voice profile or persona library is missing, say so plainly and continue —
do not fabricate either.
If the chronicle voice profile or persona library is missing, say so plainly and
continue on the Step-0 fallback — do not fabricate either.
## Step 1: Brief + calibration
@ -321,6 +336,16 @@ Settle these dimensions (most should come from context, not questions):
- **Tone** — respected-peer vs. teaching-down; calibrated to the primær.
- **Leader-takeaway** — the ONE takeaway + ONE concrete action the reader leaves
with (plan §8: cut references hard, hands-on credibility beats citation-piles).
- **Language****Resolve the review language** for this edition: detect it from
the series brief / prior editions / the drafting language, else ask once. Persist
it to `edition-state.json``language` (e.g. `"no"` for Norwegian, `"en"` for
English) at the **Step 1.5 checkpoint**. This is not cosmetic — it gates the
language-dependent review agents: `language-reviewer`'s Norwegian-idiom /
«kanselli-stil» checks and `voice-scrubber`'s gold standard fire against THIS
language, and never grade prose against Norwegian idiom when the edition is not
Norwegian. Left unresolved, the template default `"en"` silently disables the
Norwegian axis even for a Norwegian edition (the C-3 bug this closes) — so resolve
it explicitly, do not assume the default.
Use `AskUserQuestion` only for the genuinely open dimensions (cap ≈3). Good
candidates: which personas are in scope + which is primær; the angle if the
@ -446,6 +471,11 @@ Edition brief
- If Step 1 ran the **trend-intake bridge**, persist `articles.NN.sourceTrendId`
(the trend id) here too — the durable provenance link Step 10 reads to auto-`act`
the trend. Omit it (leave `null`) for a manually-started edition.
- Persist the resolved **`language`** (Step 1) — write it to the **top-level**
`language` (the series/edition-level review language; there is no per-article
`language` field) so the language-dependent review agents grade against the
right language from here on. This is the write that makes the template
doc-string's "Resolved at Step 1" claim true.
- Write `articles.NN.livedSpecifics` (the slot-map above, `status: "pending"`).
- Set `currentPhase: "lived-specifics"` in `edition-state.json`.
- Write a "lived-specifics bound → next: research (scoped by the binding)" line
@ -771,9 +801,21 @@ Typically ~2030 % of the edition's final length.
**Procedure:**
1. **Re-read the voice profile** (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`) before writing a
single sentence — this is the existing LTL rule and it is not optional for
long-form. Voice match starts at the spine, not at expansion.
1. **Pass 0 — load the voice reference + the drift negatives** before writing a
single sentence (voice match starts at the spine, not at expansion — the
existing LTL rule, not optional for long-form):
- Re-read the **chronicle-voice-profile**
(`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/chronicle-voice-profile.md`)
for this edition's language/format — the reference the spine must match
(fall back to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`
per Step 0 if it is missing).
- Read the **chronicle-voice-drift-log**
(`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/chronicle-voice-drift-log.md`)
as a **negative list**: the recurring AI-tells and voice drifts `voice-scrubber`
logged on prior editions. Do NOT reproduce them in the spine — the log is no
longer write-only; it steers drafting BEFORE the scrub, so the Step-4/persona
passes and the scrubber find less each edition. If the log is absent, note it
and proceed.
2. **For each section in `NN-skjelett.md`, write ONE paragraph that delivers
that section's pitch.** No examples yet, no anecdotes, no research citations
@ -865,8 +907,11 @@ turning-points the spine already named.
**Procedure:**
1. **Re-read the voice profile** (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`) before expanding —
the voice was set at the spine; do not lose it in expansion.
1. **Re-read the chronicle-voice-profile**
(`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/chronicle-voice-profile.md`,
fallback `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`)
and the drift-log negatives (Step 3a Pass 0) before expanding — the voice was set
at the spine; do not lose it, and do not let expansion reintroduce a logged drift.
2. **Expand section by section, against the spine.** Each section's paragraph
from Step 3a is the *contract* for that section: the expansion must
@ -1707,6 +1752,19 @@ produces the editor's single delivery artifact — `POST.html`, the
body is frozen — Step 9 may only adjust the distribution hook, never the
locked edition text.
5. **Auto-gull — grow the gold corpus (A2-F2).** A locked edition is now
published-grade voice signal, so append it to the gold corpus the chronicle
voice profile is distilled from:
- Record the locked body (`NN-utkast.md`) as a new record under
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/ingest/published/`,
tagged **`provenance=published`** (never `ai-draft`) with this edition's
`language` and format in its frontmatter, so `voice-trainer`'s
chronicle-voice-profile distillation (per language/format) picks it up.
- This closes the loop the model-collapse fence protects: the corpus the
chronicle voice is learned from is the author's OWN locked editions, growing
one edition at a time — never the engine's drafts. The next edition's Step 0
therefore binds to a sharper profile without any manual corpus curation.
```
LOCK → delivery.
- Preconditions: factcheck 🔴 = none, persona resonans primær = JA (else: STOP)