fix(linkedin-studio): N10 — konfigurerbart målnivå-spenn [skip-docs]

C-8: gjør «praktisk anvendbar på alle nivå» til en konfigurerbar gate-akse og
fjerner den hardkodede primær-personaen. 6-fils scope (KTG-valgt) — gjør
targetLevel til en EKTE ende-til-ende-dimensjon i stedet for et fantomfelt:

- references/longform-quality-rules.md: «non-technical line manager» → brukerens
  konfigurerte target-level span (domene-generelt).
- config/personas.template.md: «primær trumfer» krever nå obligatorisk sekundær i
  MOTSATT ende av spennet; sekundær-NEI kan kun avskrives som «signal, ikke svikt»
  med eksplisitt ceiling-begrunnelse. Ny tekniker-ende-seed (Persona 4,
  Løsningsutvikler/AI-ingeniør) — tidligere kun 3 leder-personas.
- config/edition-state.template.json: additivt articles.NN.targetLevel-slot
  (default null, INGEN schemaVersion-bump — samme mønster som sourceTrendId/language).
- commands/newsletter.md: persister resolvert targetLevel ved Step 1.5-checkpointet.
- agents/persona-reviewer.md: leser articles.NN.targetLevel og plasserer personaene
  på spennet (fallback til persona-ekspertise om feltet er null).

Premiss-korreksjon: planens 4-fils scope antok targetLevel fantes i edition-state
via N7-broen; det stemte ikke (fantes kun i trend-store + fritekst-mapping). +2 filer
(edition-state-slot + newsletter-persist) gjør konsumet reelt.

TDD: ny Section 16q i test-runner (9 UNCOND. .md/.json-greps + non-vacuity self-test);
en vacuity-bug i egen edition-state-check fanget og skjerpet (grep slot-literal
"targetLevel": null, ikke bare ordet — sourceTrendId-docstringen inneholder allerede
"targetLevel"). test-runner 154→163/0, anti-erosion floor 137→146. Øvrige fem suiter
uendret grønne (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-24 20:16:41 +02:00
commit 08106317db
6 changed files with 186 additions and 17 deletions

View file

@ -33,7 +33,8 @@
"pivots": "Per-article pivot log (Endring 9c). A pivot is a substantive change to a draft AFTER a gate had already cleared — e.g. a new argument anchor / section added late (the Del 4 Security Champions case: +~530 words, 2 new sections, +42 %). Each /linkedin:pivot invocation appends one entry and moves currentPhase back so the cleared gates (Steps 56.5) re-run on the pivoted version before lock. Heuristic (documented, checked at the Step 8 lock precondition): if the current draft's word count differs > 20 % from the version that last cleared Step 6, OR it has > 2 new sections, a pivot-reopen is suggested/required. Each entry: { timestamp, reason, fromPhase, toPhase, wordCountBefore, wordCountAfter, deltaPct, newSections, gatesToRerun: [phase…] }. Default [].",
"foldIns": "Per-article accumulation queue (slice 2 of fix #1 — «rettelser fester seg»). Each correction KTG makes during an edition that is NOT yet a contract rule is captured here, then routed by the JA-promoter (maskinrommet/docs/skrivekontrakt.md §E) to a permanent home so it never has to be re-discovered: a MECHANICAL correction → a rules.ts gate rule (BLOCK/WARN) + a §B-row/§C1/§C2-box + a §E-manifest row (the contract-gate `ratify` check then asserts rules.ts ↔ §E-manifest stay in bijection); a JUDGMENT correction → a §C2-box only (stays with editorial-reviewer, no gate rule). Capture is per-article (provenance = which article surfaced it); promotion is series/contract-wide. Each entry: { id, date (ISO-8601), correction (what KTG corrected, near-verbatim), trigger (where/why it surfaced), classification: \"mechanical-block\" | \"mechanical-warn\" | \"judgment\" | null (set at the classify step), decision: \"pending\" | \"promoted\" | \"rejected\" (the JA-promoter outcome), ruleId: <rules.ts id> | null (set on promote for mechanical), note?: where a judgment/rejected fold-in landed }. Default []. Rejected fold-ins are kept for traceability, never deleted.",
"language": "Review language for this series/edition (additive, default \"en\"). Threads into the long-form review agents so they grade against THIS language's rules: language-reviewer applies Norwegian-specific checks (anglicism→Norwegian idiom, «kanselli-stil») only when language == \"no\"; voice-scrubber's gold standard is the approved editions IN this language; any other value → the agents apply that language's equivalents and never grade prose against Norwegian idiom. \"no\" = Norwegian (the author's case). Resolved at Step 1 / load-context and passed to the language-dependent agents.",
"sourceTrendId": "Per-article provenance link to the trend this edition was started from (N7 trend→newsletter bridge, MR-F3). Additive-optional (default null, NO schemaVersion bump — an edition started manually never sets it, and Step 10 reads its absence as \"no source trend\"). Set at the Step 1.5 checkpoint when Step 1's trend-intake read a candidate from the trends store (scripts/trends) to prefill the brief (angle / targetLevel / key-points / source-URLs). Its ONE runtime consumer is Step 10 (scheduling): when the article reaches scheduling AND sourceTrendId is set, the command flips that trend's store status to \"acted\" (trends CLI `act --id <sourceTrendId>`), closing the discovery→production loop deterministically instead of by hand. Value = the trend's store id (normalized title+url, as shown in the /linkedin:trends brief and `CLI list --json`)."
"sourceTrendId": "Per-article provenance link to the trend this edition was started from (N7 trend→newsletter bridge, MR-F3). Additive-optional (default null, NO schemaVersion bump — an edition started manually never sets it, and Step 10 reads its absence as \"no source trend\"). Set at the Step 1.5 checkpoint when Step 1's trend-intake read a candidate from the trends store (scripts/trends) to prefill the brief (angle / targetLevel / key-points / source-URLs). Its ONE runtime consumer is Step 10 (scheduling): when the article reaches scheduling AND sourceTrendId is set, the command flips that trend's store status to \"acted\" (trends CLI `act --id <sourceTrendId>`), closing the discovery→production loop deterministically instead of by hand. Value = the trend's store id (normalized title+url, as shown in the /linkedin:trends brief and `CLI list --json`).",
"targetLevel": "Per-article target-level for the edition (N10 / C-8). Additive-optional (default null, NO schemaVersion bump — same pattern as sourceTrendId/language; an edition that never resolves one leaves it null and the reader agents fall back to each persona's own `ekspertise` field). Fixes WHERE this edition sits on the configured target-level span (references/longform-quality-rules.md + config/personas.template.md): it tells persona-reviewer which end is the reader-facing (primær) end and which is the mandatory-secondary end, so «practically usable at all levels» is judged from both ends rather than one baked-in reader role. Set at Step 1 of /linkedin:newsletter — the N7 trend bridge / N6 proposal layer pre-fills it from the trend record's `targetLevel` (scripts/trends), the operator confirms or edits it during calibration, and it is persisted here at the Step 1.5 checkpoint (alongside sourceTrendId). Free-text level descriptor (e.g. \"line leader → solution engineer\", \"beginner → practitioner\"), NOT an enum — the span is domain-general and defined by the operator's persona set, never hardcoded."
},
"schemaVersion": 1,
"series": {
@ -92,7 +93,8 @@
"foldIns": [],
"locked": false,
"scheduled": null,
"sourceTrendId": null
"sourceTrendId": null,
"targetLevel": null
}
}
}

View file

@ -13,7 +13,7 @@ cp config/personas.template.md config/personas.local.md
```
`personas.local.md` is gitignored (via `*.local.md`) so your active overrides
stay local. The template ships the three Seres seed personas below; clone,
stay local. The template ships the four Seres seed personas below; clone,
trim, or extend them per series.
---
@ -23,11 +23,16 @@ trim, or extend them per series.
- **Per-project selection.** `/linkedin:newsletter` (Step 1) picks the relevant
personas from this library and marks the primary in the edition brief.
- **«primær trumfer».** Exactly one persona is the **primær** reader. On
conflict between personas, the primær weighs highest. A *secondary* NO caused
by role mismatch or an expertise ceiling («this I already know cold») is a
SIGNAL that the gate works — accept it, do not distort the text to chase it.
A *primær* NO is **not** accepted: the text is revised until the primær
reaches a clean YES.
conflict between personas, the primær weighs highest. But the active set MUST
include a **mandatory secondary at the opposite end of the target-level span**
from the primær (a technical reader when the primær is non-technical, and vice
versa) — one reader alone cannot certify a text «practically usable at all
levels», so a lone primær JA is not enough. A *secondary* NO caused by role
mismatch or an expertise ceiling («this I already know cold») is a SIGNAL that
the gate works — but it may be waived as «signal, not failure» **only with an
explicit ceiling justification** that names which end of the span hit its
ceiling; an unexplained secondary NO is a real flag, not a free pass. A *primær*
NO is **not** accepted: the text is revised until the primær reaches a clean YES.
- **Two sweep modes** (same `persona-reviewer` agent): resonance mode (Step 6,
BEFORE lock — «does the point land for this reader?») and conversion mode
(Step 9, after lock — binary «would YOU click?» on the hook only).
@ -64,8 +69,9 @@ can be promoted to a reusable block by pasting them into `personas.local.md`
The persona sweep is not advisory — it returns a **blocking verdict**
(PASS / REWORK / BLOCK), and the bar is the **primær reader's genuine, unqualified
JA**. The three Seres seed personas are the canonical set: **A = IT-divisjonsdirektør**
(sekundær), **B = KI-seksjonsleder** (sekundær), **C = Linjeleder** (PRIMÆR — trumfer).
JA**. The four Seres seed personas are the canonical set: **A = IT-divisjonsdirektør**
(sekundær), **B = KI-seksjonsleder** (sekundær), **C = Linjeleder** (PRIMÆR — trumfer),
**D = Løsningsutvikler/AI-ingeniør** (sekundær — the mandatory technical end opposite C).
- **Bar = C ekte JA.** A clean, unqualified yes from the primær. **«JA med store
forbehold» = NEI.**
@ -143,6 +149,27 @@ pipeline and the structural check key off them:
- **sjargong** — Lav toleranse for teknisk sjargong; setter pris på presise,
hverdagsnære formuleringer.
### Persona 4 — Løsningsutvikler/AI-ingeniør (sekundær)
> **Technical end of the target-level span** (opposite the primær persona). The
> mandatory secondary the «primær trumfer» rule requires: if the primær (Linjeleder)
> anchors the reader-facing end, this persona anchors the technical end, so a text
> that claims to work «at all levels» is judged from both ends, not just one.
- **rolle** — Bygger og drifter AI-løsningene i praksis (utvikler, ML-ingeniør eller
løsningsarkitekt); skriver koden, velger modellene, eier integrasjonene.
- **avkobler** — Ledelses-abstraksjoner uten teknisk substans; «AI-magi» uten hvordan;
påstander om hva som er mulig som ikke tåler et implementasjons-blikk; nedlatende
forenkling av ting hen kan fra innsiden.
- **overbeviser** — Konkret hvordan-det-faktisk-virker, ærlige begrensninger og
feilmodi, reelle avveininger (latency/kost/kvalitet), og respekt for at
dømmekraften i systemdesign er ekte fag.
- **ekspertise** — Høy teknisk. Ekspertise-tak på grunnleggende forklaringer: en
«hva er en API»-passasje faller flatt (sekundær-NEI her er et signal — men bare
med eksplisitt ceiling-begrunnelse, jf. «primær trumfer»).
- **sjargong** — Høy toleranse for teknisk/kode-sjargong; lav for lederspråk og
konsulent-buzzwords.
---
## Adding a persona