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

@ -98,6 +98,18 @@ Read the named persona from `config/personas.template.md` (or the project's
**overbeviser**, **ekspertise**, **sjargong**. Judge as that reader — not as **overbeviser**, **ekspertise**, **sjargong**. Judge as that reader — not as
yourself, not as a generic audience. One run = one persona. yourself, not as a generic audience. One run = one persona.
Then read `articles.NN.targetLevel` from the edition-state (set at Step 1 of
`/linkedin:newsletter` — the N7 trend bridge / N6 proposal layer pre-fills it,
the operator confirms it during calibration). It fixes where this persona sits on
**the edition's configured target-level span**: the primær anchors the
reader-facing end, and the active set MUST include a mandatory secondary at the
OPPOSITE end of that span. Place this persona on the span accordingly — a
secondary NO from a reader at the far end is a SIGNAL only when the persona
genuinely sits at that end, and only with an explicit ceiling justification;
otherwise it is a real flag. If `articles.NN.targetLevel` is absent (a
manually-started edition that never resolved one), fall back to the persona's own
**ekspertise** field to locate it on the span.
### Step 2: Read the input as that reader ### Step 2: Read the input as that reader
Read top to bottom, read-only, once, the way this reader actually would. Read top to bottom, read-only, once, the way this reader actually would.

View file

@ -300,7 +300,10 @@ what genuinely changes the work.
> point the operator confirms or edits* in the dimensions below, not a bypass of > point the operator confirms or edits* in the dimensions below, not a bypass of
> calibration: > calibration:
> - `angle` → the edition **Angle**. > - `angle` → the edition **Angle**.
> - `targetLevel` → the audience/tone calibration (**Audience** / **Tone**). > - `targetLevel` → the audience/tone calibration (**Audience** / **Tone**); the
> resolved value is also persisted to `articles.NN.targetLevel` at the Step 1.5
> checkpoint (below) so persona-reviewer can place the personas on the configured
> target-level span (which end is primær, which is the mandatory secondary).
> - `summary` + `rationale` + `readerQuestion` / `painPoint` → seed the **Key > - `summary` + `rationale` + `readerQuestion` / `painPoint` → seed the **Key
> points** and the **Leader-takeaway** (the reader-side framing this bridge > points** and the **Leader-takeaway** (the reader-side framing this bridge
> exists to carry: what the reader would DO with it). > exists to carry: what the reader would DO with it).
@ -471,6 +474,12 @@ Edition brief
- If Step 1 ran the **trend-intake bridge**, persist `articles.NN.sourceTrendId` - 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 id) here too — the durable provenance link Step 10 reads to auto-`act`
the trend. Omit it (leave `null`) for a manually-started edition. the trend. Omit it (leave `null`) for a manually-started edition.
- Persist the resolved **`targetLevel`** (Step 1's Audience/Tone calibration; the
trend bridge pre-fills it from the trend record) to `articles.NN.targetLevel` — the
per-article position on the configured target-level span that persona-reviewer reads
to place the personas (which end is primær, which is the mandatory secondary). Leave
`null` if the operator did not resolve a level; the reader agents then fall back to
each persona's own `ekspertise` field.
- Persist the resolved **`language`** (Step 1) — write it to the **top-level** - 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` (the series/edition-level review language; there is no per-article
`language` field) so the language-dependent review agents grade against the `language` field) so the language-dependent review agents grade against the

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 [].", "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.", "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.", "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, "schemaVersion": 1,
"series": { "series": {
@ -92,7 +93,8 @@
"foldIns": [], "foldIns": [],
"locked": false, "locked": false,
"scheduled": null, "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 `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. 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 - **Per-project selection.** `/linkedin:newsletter` (Step 1) picks the relevant
personas from this library and marks the primary in the edition brief. personas from this library and marks the primary in the edition brief.
- **«primær trumfer».** Exactly one persona is the **primær** reader. On - **«primær trumfer».** Exactly one persona is the **primær** reader. On
conflict between personas, the primær weighs highest. A *secondary* NO caused conflict between personas, the primær weighs highest. But the active set MUST
by role mismatch or an expertise ceiling («this I already know cold») is a include a **mandatory secondary at the opposite end of the target-level span**
SIGNAL that the gate works — accept it, do not distort the text to chase it. from the primær (a technical reader when the primær is non-technical, and vice
A *primær* NO is **not** accepted: the text is revised until the primær versa) — one reader alone cannot certify a text «practically usable at all
reaches a clean YES. 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, - **Two sweep modes** (same `persona-reviewer` agent): resonance mode (Step 6,
BEFORE lock — «does the point land for this reader?») and conversion mode BEFORE lock — «does the point land for this reader?») and conversion mode
(Step 9, after lock — binary «would YOU click?» on the hook only). (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** 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 (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** 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). (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 - **Bar = C ekte JA.** A clean, unqualified yes from the primær. **«JA med store
forbehold» = NEI.** forbehold» = NEI.**
@ -143,6 +149,27 @@ pipeline and the structural check key off them:
- **sjargong** — Lav toleranse for teknisk sjargong; setter pris på presise, - **sjargong** — Lav toleranse for teknisk sjargong; setter pris på presise,
hverdagsnære formuleringer. 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 ## Adding a persona

View file

@ -31,8 +31,10 @@ they will do about it.
- If the reader cannot state the takeaway in one sentence, the text is not done — - If the reader cannot state the takeaway in one sentence, the text is not done —
tighten until they can. tighten until they can.
- **Serve what the reader can DO, not what the author knows.** The text exists to - **Serve what the reader can DO, not what the author knows.** The text exists to
give the *primær* reader (the non-technical line manager) something they can act give the *primær* reader (whoever sits at the reader-facing end of the edition's
on from their own chair — not to demonstrate the author's breadth. Completeness configured **target-level span** — see `config/personas.template.md`) something
they can act on from their own chair — not to demonstrate the author's breadth.
Completeness
is not a virtue: if a passage exists to be thorough rather than to move the is not a virtue: if a passage exists to be thorough rather than to move the
reader's decision forward, cut it. Choose ONE concrete, verifiable (preferably reader's decision forward, cut it. Choose ONE concrete, verifiable (preferably
Norwegian) case over an exhaustive list. Norwegian) case over an exhaustive list.

View file

@ -67,8 +67,13 @@
# non-vacuity self-test) in Section 16n; the randsone-gates guard (N9: newsletter.md Step 9 runs a scoped # non-vacuity self-test) in Section 16n; the randsone-gates guard (N9: newsletter.md Step 9 runs a scoped
# fact-reviewer + language-reviewer pass over the carousel slide text AND edition title, gates title-strength, # fact-reviewer + language-reviewer pass over the carousel slide text AND edition title, gates title-strength,
# declares the deterministic 'numbers, names, or quotes' recheck trigger, and both fact agents carry WebFetch, # declares the deterministic 'numbers, names, or quotes' recheck trigger, and both fact agents carry WebFetch,
# with a non-vacuity self-test) in Section 16p; the assertion-count anti-erosion floor (SC6) in Section 18. All # with a non-vacuity self-test) in Section 16p; the configurable target-level span guard (N10: the hardcoded
# are live below (Sections 818). # 'non-technical line manager' is gone from references/ + config/, references/longform-quality-rules.md names
# the 'target-level span', config/personas.template.md requires a mandatory secondary at the opposite end +
# an explicit ceiling justification + seeds a technical-end persona, config/edition-state.template.json carries
# the additive articles.NN.targetLevel slot, commands/newsletter.md persists it, agents/persona-reviewer.md
# consumes it onto the span, with a non-vacuity self-test) in Section 16q; the assertion-count anti-erosion
# floor (SC6) in Section 18. All are live below (Sections 818).
# #
# Usage: bash scripts/test-runner.sh # Usage: bash scripts/test-runner.sh
# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`. # bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`.
@ -1691,6 +1696,114 @@ fi
echo "" echo ""
# --- Section 16q: Configurable Target-Level Span (N10 / C-8) ---
echo "--- Configurable Target-Level Span (N10) ---"
# N10 turns "practically usable at all levels" into a CONFIGURABLE gate axis and removes the
# hardcoded primary persona. Six .md/.json surfaces must hold the invariants (all UNCONDITIONAL
# greps -> they raise the deps-absent floor):
# (C-8 absence) the hardcoded primary "non-technical line manager" is GONE from references/ + config/
# — the quality-rule now points at the edition's CONFIGURED target-level span instead.
# (C-8 span-ref) references/longform-quality-rules.md names the "target-level span" (domain-general),
# not a single baked-in reader role.
# (C-8 obligatorisk-sekundær) config/personas.template.md's «primær trumfer» rule now REQUIRES a
# mandatory secondary at the opposite end of the span; a secondary NO may be waived
# as «signal, not failure» ONLY with an explicit ceiling justification.
# (C-8 tekniker-seed) the seed library gains a technical-end persona (today all three seeds are leaders).
# (C-8 edition-state) config/edition-state.template.json carries an additive per-article targetLevel slot.
# (C-8 persist) commands/newsletter.md persists the resolved targetLevel to articles.NN.targetLevel.
# (C-8 consume) agents/persona-reviewer.md READS articles.NN.targetLevel and places personas on the span
# — a compound predicate (targetLevel field + edition-state read + span placement), so a
# non-vacuity self-test guards it (mirrors Section 16p).
PR_N10="agents/persona-reviewer.md"
targetlevel_consumed() { # $1 = text; wired iff it reads targetLevel FROM edition-state AND places on the span
echo "$1" | grep -qF "targetLevel" \
&& echo "$1" | grep -qF "articles." \
&& echo "$1" | grep -qF "target-level span"
}
TL_SELFTEST_OK=1
if ! targetlevel_consumed "read articles.NN.targetLevel and place the personas on the target-level span"; then
TL_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired targetLevel-consumption probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if targetlevel_consumed "$probe"; then
TL_SELFTEST_OK=0; echo " false-positive FAIL: under-wired targetLevel probe accepted -> $probe"
fi
done <<'NEGATIVE16Q'
place the personas on the target-level span but never read the field
read articles.NN.targetLevel but do not mention the span placement here
the targetLevel matters for the target-level span without an edition-state read
NEGATIVE16Q
if [ "$TL_SELFTEST_OK" -eq 1 ]; then
pass "targetLevel-consumption self-test: predicate needs targetLevel + edition-state read (articles.) + span placement (1 accepted, 3 under-wired rejected)"
else
fail "targetLevel-consumption self-test failed - the N10 consumption lint is vacuous or over-eager"
fi
# (C-8 absence) the hardcoded primary persona is gone from the shippable surfaces
if grep -rn "non-technical line manager" references/ config/ >/dev/null 2>&1; then
fail "N10: hardcoded 'non-technical line manager' still present in references/ or config/ — the primary persona is not de-hardcoded (C-8)"
else
pass "N10: no hardcoded 'non-technical line manager' in references/ or config/ (C-8 absence)"
fi
# (C-8 span-ref) the quality rule points at the configured span, not a baked-in role
if grep -qF "target-level span" references/longform-quality-rules.md; then
pass "references/longform-quality-rules.md names the configured 'target-level span' (C-8 span-ref)"
else
fail "references/longform-quality-rules.md missing 'target-level span' — the served-reader rule is still role-hardcoded (C-8)"
fi
# (C-8 obligatorisk-sekundær) the «primær trumfer» rule requires a mandatory secondary at the opposite end
if grep -qF "opposite end of the target-level span" config/personas.template.md; then
pass "config/personas.template.md requires a mandatory secondary at the opposite end of the span (C-8 obligatorisk-sekundær)"
else
fail "config/personas.template.md missing the 'opposite end of the target-level span' requirement — one reader can still self-certify all levels (C-8)"
fi
# (C-8 ceiling-begrunnelse) a secondary NO may only be waived with an explicit ceiling justification
if grep -qF "explicit ceiling justification" config/personas.template.md; then
pass "config/personas.template.md gates the secondary-NO waiver behind an explicit ceiling justification (C-8 ceiling-begrunnelse)"
else
fail "config/personas.template.md missing 'explicit ceiling justification' — a secondary NO can still be waved away as 'signal, not failure' unproven (C-8)"
fi
# (C-8 tekniker-seed) the seed library gains a technical-end persona (today all three seeds are leaders)
if grep -qF "Technical end of the target-level span" config/personas.template.md; then
pass "config/personas.template.md seeds a technical-end persona opposite the leader personas (C-8 tekniker-seed)"
else
fail "config/personas.template.md missing a 'Technical end of the target-level span' seed — the library is still leader-only (C-8)"
fi
# (C-8 edition-state) the per-article targetLevel SLOT exists (additive, no schemaVersion bump).
# Grep the slot's default literal `"targetLevel": null`, NOT the bare word — the latter already occurs
# inside the sourceTrendId doc-string ("angle / targetLevel / key-points"), which would match vacuously.
if grep -qF '"targetLevel": null' config/edition-state.template.json; then
pass "config/edition-state.template.json carries the additive articles.NN.targetLevel slot (default null, C-8 edition-state)"
else
fail "config/edition-state.template.json missing the '\"targetLevel\": null' slot — persona-reviewer would consume a phantom field (C-8)"
fi
# (C-8 persist) newsletter.md persists the resolved targetLevel to the per-article slot
if grep -qF "articles.NN.targetLevel" commands/newsletter.md; then
pass "commands/newsletter.md persists the resolved targetLevel to articles.NN.targetLevel (C-8 persist)"
else
fail "commands/newsletter.md never persists articles.NN.targetLevel — the slot stays null and consumption is vacuous (C-8)"
fi
# (C-8 consume) persona-reviewer reads targetLevel from edition-state and places personas on the span
PR_CONTENT=$(cat "$PR_N10")
if targetlevel_consumed "$PR_CONTENT"; then
pass "agents/persona-reviewer.md reads articles.NN.targetLevel and places personas on the target-level span (C-8 consume)"
else
fail "agents/persona-reviewer.md does not consume articles.NN.targetLevel onto the span — targetLevel is set but never read (C-8)"
fi
echo ""
# --- Section 18: Assertion-Count Anti-Erosion (SC6) --- # --- Section 18: Assertion-Count Anti-Erosion (SC6) ---
# 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
@ -1725,12 +1838,16 @@ echo ""
# Step-8 auto-gull compound grep) = 129; +8 for N9's eight UNCONDITIONAL Section-16p checks (randsone-gate # Step-8 auto-gull compound grep) = 129; +8 for N9's eight UNCONDITIONAL Section-16p checks (randsone-gate
# self-test + Step-9 randsone_gated compound grep + Step-9 title-strength grep + newsletter C-6 trigger-nouns # self-test + Step-9 randsone_gated compound grep + Step-9 title-strength grep + newsletter C-6 trigger-nouns
# grep + newsletter C-6 scoped-fact-reviewer grep + newsletter C-6 determinism-marker grep + fact-checker # grep + newsletter C-6 scoped-fact-reviewer grep + newsletter C-6 determinism-marker grep + fact-checker
# WebFetch grep + fact-reviewer WebFetch grep) = 137. # WebFetch grep + fact-reviewer WebFetch grep) = 137; +9 for N10's nine UNCONDITIONAL Section-16q checks
# (targetLevel-consumption self-test + no-hardcoded-line-manager absence grep + references target-level-span
# grep + personas opposite-end obligatorisk-sekundær grep + personas explicit-ceiling-justification grep +
# personas technical-end seed grep + edition-state targetLevel-slot grep + newsletter articles.NN.targetLevel
# persist grep + persona-reviewer targetLevel-consumption compound grep) = 146.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop # 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 # 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 # 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. # clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=137 ASSERT_BASELINE_FLOOR=146
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"