From e26d9fb659bda9b7402274268bb2f0d1424e4db9 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 20 Jun 2026 20:34:49 +0200 Subject: [PATCH] feat(linkedin-studio): wire contract-gate + ratify into newsletter Step 4.5 (fix #1 slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 3 of fix #1 (§C2 bindende + akkumulerende): wire the deterministic §B/§C1 contract-gate into the long-form pipeline as a pre-handoff step, so mechanical violations die deterministically BEFORE the costly AI gates and never reach the operator at a judgment gate ("rettelser fester seg"). Newsletter pipeline: - New Step 4.5 (between Step 4 consistency/quality and Step 5 fact-check): (1) ratify the gate (rules.ts <-> §E-manifest 1:1; DRIFT escalates to operator, not a draft fix) and (2) gate the draft (exit 0 PASS / exit 1 BLOCK -> fix by tightening + re-run; WARN = direction, never blocks). Adopter-safe: no-op when contract-gate deps / the maskinrommet contract are absent. - Pipeline phase count 16 -> 17 across every LIVE surface (overview table + build-status + narrative + banner + schema pointer in newsletter.md; command-table rows in CLAUDE.md and README). Historical version-narrative entries left untouched. - edition-state.template.json: new "contract-gate" phase string (Step 4.5), between consistency-quality and factcheck-sweep. JSON valid; phases 16->17. CI integration (test-runner.sh, Section 14 — new): - 14a: contract-gate suite stays green (npm test exit 0) with a 33-test case-count floor (anti-erosion). - 14b: ratify invariant — rules.ts bound 1:1 to the §E-manifest. - Both KTG-internal and adopter-safe (warn/skip, never fail, when deps or the contract are absent). set +e inside the test command-substitution keeps a red suite from aborting the runner; ratify exit 1 captured via if-subshell. - SC6 anti-erosion section renumbered 14 -> 15; header comment updated. Verification: - Structure lint 81 -> 83/0/0 (contract-gate suite + ratify both green). - Documented Step 4.5 command smoke-tested on a real draft (serier/silvija-seres-motsvar/utkast/04-utkast.md): 0 BLOCK / 2 WARN -> "Gate passert", exit 0. - edition-state.template.json valid JSON; phases array = 17. foldIns capture into the pipeline phases deferred to a later slice (scope A). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- README.md | 4 +- commands/newsletter.md | 88 +++++++++++++++++++++++++++--- config/edition-state.template.json | 1 + scripts/test-runner.sh | 51 ++++++++++++++++- 5 files changed, 131 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index da53d03..809c609 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ All content commands (post, quick, react, pipeline, first-post, video, multiplat | `/linkedin:post` | Full post creation (10-15 min) | | `/linkedin:quick` | 5-minute quick post (3-line formula) + 8 post-type templates | | `/linkedin:pipeline` | Full end-to-end content pipeline | -| `/linkedin:newsletter` | Long-form orchestrator: newsletter edition / essay / series article — multi-session 16-phase pipeline with **skeleton + spine-prose gates BEFORE prose (v2.1)**, **editorial-review craft gate BEFORE the persona sweep (Step 5.5, v2.4)**, fact-check + persona-sweep BEFORE lock, **headless adversarial review BEFORE lock (Step 6.5, v3.1)**, and **visual-assets gate BEFORE lock (Step 7.5, v2.3)** | +| `/linkedin:newsletter` | Long-form orchestrator: newsletter edition / essay / series article — multi-session 17-phase pipeline with **skeleton + spine-prose gates BEFORE prose (v2.1)**, a **deterministic §B/§C1 contract-gate BEFORE the AI sweeps (Step 4.5)**, **editorial-review craft gate BEFORE the persona sweep (Step 5.5, v2.4)**, fact-check + persona-sweep BEFORE lock, **headless adversarial review BEFORE lock (Step 6.5, v3.1)**, and **visual-assets gate BEFORE lock (Step 7.5, v2.3)** | | `/linkedin:headless-review` | **(v3.1)** Cold adversarial review package — run the 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` (resonance/conversion) on a FROZEN draft with no drafting-session context; consolidated, operator-gated report. Step 6.5's standalone surface (run in a fresh session for maximum isolation) | | `/linkedin:pivot` | **(v3.1)** Re-open a long-form edition after a late substantive change so cleared gates (fact-check → editorial → persona → headless) re-run before lock; logs `pivots[]`, resets `currentPhase`, un-locks if needed (pivot heuristic: >20 % word-count change or >2 new sections) | | `/linkedin:batch` | Create a full week of content | diff --git a/README.md b/README.md index 892ac2d..5f4ba7d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ For everyday presence. The point is *velocity without losing quality*: every con ### 📖 The Long-Form Engine — `/linkedin:newsletter` -For the pieces that build authority — newsletter editions, essays, series articles. This is what sets LinkedIn Studio apart from "AI writes your post" tools: a **16-phase pipeline where the draft has to survive a gauntlet of quality gates *before it locks*.** +For the pieces that build authority — newsletter editions, essays, series articles. This is what sets LinkedIn Studio apart from "AI writes your post" tools: a **17-phase pipeline where the draft has to survive a gauntlet of quality gates *before it locks*.** ``` skeleton gate ──▶ voice scrub ──▶ fact-check ──▶ editorial craft gate ──▶ @@ -133,7 +133,7 @@ All 29 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. Th | `/linkedin:multiplatform` | Adapt LinkedIn content for X threads, newsletter sections, blog posts, slides, YouTube scripts. | | `/linkedin:batch` | A full week of content in one session — one theme in, 3–5 posts out, written to the queue. | | `/linkedin:pipeline` | Full end-to-end pipeline from idea to published post (ideation → publish → post-analysis). | -| `/linkedin:newsletter` | Long-form orchestrator — newsletter/essay/series article at series quality. Multi-session 16-phase pipeline; all gates BEFORE lock. | +| `/linkedin:newsletter` | Long-form orchestrator — newsletter/essay/series article at series quality. Multi-session 17-phase pipeline; all gates BEFORE lock. | | `/linkedin:headless-review` | Cold adversarial review package on a FROZEN draft (`content-reviewer` + `language-reviewer` + `fact-reviewer` + `persona-reviewer`) — run in a fresh session for maximum isolation. | | `/linkedin:pivot` | Re-open a long-form edition after a substantive late change so cleared gates re-run before lock (heuristic: >20 % word-count or >2 new sections). | diff --git a/commands/newsletter.md b/commands/newsletter.md index 0187437..ec3dcc0 100644 --- a/commands/newsletter.md +++ b/commands/newsletter.md @@ -22,9 +22,10 @@ allowed-tools: # LinkedIn Newsletter — Long-Form Content Engine > **⏱ Before you start — this is a campaign, not a post.** Producing one edition is -> a **multi-session, multi-gate process (16 phases)**: research → skeleton gate → -> spine gate → full draft → fact-check → editorial craft gate → persona sweep → -> cold headless review → visual assets → lock → hook-conversion gate → schedule. +> a **multi-session, multi-gate process (17 phases)**: research → skeleton gate → +> spine gate → full draft → contract-gate → fact-check → editorial craft gate → +> persona sweep → cold headless review → visual assets → lock → hook-conversion +> gate → schedule. > Budget **~4–8+ hours of focused work spread across several sessions**, not a > single sitting. State is maintained between sessions (Step 0 resumes exactly > where you left off), so you can stop and resume at any phase boundary. If you @@ -81,7 +82,7 @@ delegate the fan-out to a nested background agent. > only layer that can reliably spawn parallel sub-agents. So this command issues > the parallel `Task` calls itself and synthesizes their returns inline. -## Pipeline overview (16 phases) +## Pipeline overview (17 phases) The phase order is fixed. Two gates run **BEFORE prose** (skeleton + spine prose), an **editorial craft gate** runs before the persona sweep, the persona @@ -100,6 +101,7 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias). | 3a | **Spine prose — BEFORE full expansion** | one paragraph per section carrying that section's pitch, nothing more. ~20–30 % of final length. Operator-gate on whether the axis is right now that there is prose on it. | inline drafting + `content-repurposer` | | 3b | **Full prose expansion** | expand each section with argument, examples, anchors from research; may span sessions | `content-repurposer` + `Task` | | 4 | **Consistency + quality** | threads, premise→conclusion arc, leader-takeaway, AI-slop removal, de-AI/voice scrub, formatting dose | inline + `references/longform-quality-rules.md` + **`voice-scrubber`** | +| 4.5 | **Contract-gate — BEFORE the AI sweeps** | deterministic §B/§C1 rule-gate on the draft: ratify `rules.ts` ↔ §E-manifest (KTG invariant) → gate the draft (BLOCK = fix by tightening + re-run; WARN = direction). Mechanical violations die here, before the costly AI gates. | `scripts/contract-gate` (`node --import tsx src/cli.ts`) | | 5 | **Fact-check sweep** | risk-sorted (🔴/🟡/🟢), guilty-until-disproven, verification log | **`fact-checker` (parallel)** | | 5.5 | **Editorial review — BEFORE persona sweep** | editor's craft gate: prose-craft (em-dash density, verbatim repetition, postulated numbers, contradictions, versal-tic) + narrative-architecture (concrete instantiation, theory-anchored hypotheses, series-title symmetry, equal action per addressee, un-overloaded conclusion). ≤10 flags, BLOCK/REWORK/NICE. Operator-gated via `SendUserFile`. | **`editorial-reviewer`** + `SendUserFile` | | 6 | **Persona sweep — BEFORE lock** | reader jury, primær wins, convergence to clean YES | **`persona-reviewer`** (resonance mode) | @@ -110,11 +112,11 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias). | 9 | **Hook / conversion gate** | persona gate on the distribution text post-lock: "would YOU click?" | **`persona-reviewer`** (conversion mode) | | 10 | **Scheduling** | register the edition in the plugin queue/state for native scheduling | `hooks/scripts/queue-manager.mjs` | -> **Build status:** all 16 phases (Steps 0–2.5, 3a, 3b, 4, 5, 5.5, 6, 6.5, 7, -> 7.5, 8–10) are implemented below. This command takes an edition end-to-end: +> **Build status:** all 17 phases (Steps 0–2.5, 3a, 3b, 4, 4.5, 5, 5.5, 6, 6.5, +> 7, 7.5, 8–10) are implemented below. This command takes an edition end-to-end: > load → calibration → verified research → **skeleton + section pitch (operator + > persona gate BEFORE prose)** → **spine prose (operator gate BEFORE full -> expansion)** → full prose draft → consistency/quality → fact-check sweep → +> expansion)** → full prose draft → consistency/quality → **contract-gate (deterministic §B/§C1)** → fact-check sweep → > **editorial review (craft gate, operator-gated BEFORE the persona sweep)** → > pre-lock persona sweep → **headless adversarial review (cold review package, > operator-gated BEFORE lock)** → optional annotation → **visual assets @@ -735,7 +737,7 @@ modell-/navne-katalog collapse) to the operator. Voice-MATCH remains non-self-certified — that verdict stays with the Step 6 persona sweep / operator. After the pass, set `currentPhase: "consistency-quality"` in `edition-state.json` -and write a "quality pass complete → next: fact-check sweep" line to +and write a "quality pass complete → next: contract-gate" line to `/STATE.md` (overwrite). ``` @@ -746,6 +748,74 @@ Consistency + quality pass complete. - AI-slop phrases removed: - Formatting dose: - Length delta vs. draft: (target: flat) +Next: Step 4.5 — Contract-gate (deterministic §B/§C1 binding, BEFORE the AI sweeps). +``` + +## Step 4.5: Contract-gate — deterministic §B/§C1 binding (BEFORE the AI sweeps) + +Before any AI gate touches the draft — fact-check (Step 5), editorial craft (Step +5.5), persona sweep (Step 6), headless review (Step 6.5) — run the **deterministic +contract-gate** over the consistency-passed draft. It is pure regex/literal matching +(no model in the loop): the same mechanical §B/§C1 violation is caught and fixed the +same way every time, so it **never reaches the operator at a judgment gate**. This is +the machine half of the "corrections that stick" goal — the cheap, deterministic pass +runs first so the costly AI gates spend their judgment on resonance and truth, not on +mechanical noise. + +> **Truth source — §B/§C1 vs §C2.** The gate's ruleset is `scripts/contract-gate/ +> src/rules.ts` (data, not code), bound **1:1** to the Maskinrommet skrivekontrakt +> **§E gate-bindings-manifest** by `ratify` (id ↔ ref ↔ severity bijection). It is the +> *mechanical* half of the contract (§B structure + §C1 mechanical gate); the *craft* +> half (§C2) is the `editorial-reviewer` at Step 5.5. This Step does not restate the +> rules — `rules.ts` is the single source of truth, and ratify proves it stays bound +> to the contract. Distinct from Step 4's `references/longform-quality-rules.md`, +> which is the §A/quality reference applied by reasoning. + +> **Adopter note.** The gate is a KTG-internal tool: it needs the contract-gate deps +> (`scripts/contract-gate/node_modules` — run `npm install` there once) and, for +> ratify, the maskinrommet contract. If either is absent (a plain adopter ships +> neither), this Step is a **no-op** and the §B/§C1 discipline falls back to the Step 4 +> quality-pass reasoning. It never blocks an adopter. + +**Procedure:** + +1. **Ratify the gate (invariant — confirm it is still bound to the contract).** Before + trusting any verdict, confirm the ruleset is in 1:1 sync with the contract + §E-manifest: + ``` + cd "${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate" && \ + node --import tsx src/cli.ts --ratify + ``` + - **exit 0** → `rules.ts` bound 1:1 to §E → proceed. + - **exit 1 (DRIFT)** → a **tooling/contract inconsistency** (a rule changed on one + side only). This is NOT a draft fix — **escalate to the operator**; the §E + accumulation procedure (promote → ratify) reconciles it. `[GATE]` + - contract or deps absent → skip with a one-line note (adopter); the draft-gate + below still runs if the deps are present. + +2. **Gate the draft.** Run the gate over the edition draft. The gate runs from its own + dir (where `tsx` lives), so pass the draft's **absolute** path: + ``` + cd "${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate" && \ + node --import tsx src/cli.ts "/NN-utkast.md" + ``` + - **exit 0** → PASS. Surface any **WARN** violations as direction (operator + judgment — WARN never blocks). + - **exit 1** → BLOCKED. Show each BLOCK violation (ruleId · ref · line · message), + fix **by tightening** (the Step 4 rule still holds — swap weaker for sharper, do + not expand), and **re-run until exit 0**. `[GATE]` + - **exit 2** → usage error (e.g. wrong path) — diagnose; do not treat as a content + flag. + +3. **Persist + checkpoint state.** Set `currentPhase: "contract-gate"` in + `edition-state.json` and write a "contract-gate passed → next: fact-check sweep" + line to `/STATE.md` (overwrite). + +``` +Contract-gate complete. +- Ratify: +- BLOCK violations: <0 / fixed N by tightening> +- WARN violations: Next: Step 5 — Fact-check sweep (guilty-until-disproven, BEFORE lock). ``` @@ -1572,7 +1642,7 @@ the honest decision surface; it sells nothing. ## Reference Files -- `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json` — edition-state schema (16 phases including v2.1 skeleton + spine-prose gates, v2.3 visual-assets, v2.4 editorial-review, and v3.1 headless-review + per-article `personas` + `pivots`) +- `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json` — edition-state schema (17 phases including the deterministic §B/§C1 contract-gate (Step 4.5), v2.1 skeleton + spine-prose gates, v2.3 visual-assets, v2.4 editorial-review, and v3.1 headless-review + per-article `personas` + `pivots`) - `${CLAUDE_PLUGIN_ROOT}/config/edition-config.template.json` — static delivery metadata schema (calendar, freshness, credit, captions) — Step 8 - `${CLAUDE_PLUGIN_ROOT}/config/image-credit-caption.template.md` — cover motif + credit + caption table (honest-about-AI credit) — Step 7.5 - `${CLAUDE_PLUGIN_ROOT}/config/edition-delingstekst.template.md` — distribution-copy grammar (`## Del N —` / `## Samle`) — Steps 8/9 diff --git a/config/edition-state.template.json b/config/edition-state.template.json index 7177a93..83692cd 100644 --- a/config/edition-state.template.json +++ b/config/edition-state.template.json @@ -12,6 +12,7 @@ "spine-prose — one paragraph per section against the gated skeleton, operator gate BEFORE full expansion (Step 3a)", "draft — full prose expansion against the gated spine; may span sessions (Step 3b)", "consistency-quality — threads, premise→conclusion arc, AI-slop removal, formatting dose (Step 4)", + "contract-gate — deterministic §B/§C1 rule-gate over the draft (scripts/contract-gate, rules.ts), bound 1:1 to the maskinrommet skrivekontrakt §E-manifest via ratify; mechanical violations die here before the AI sweeps (Step 4.5)", "factcheck-sweep — risk-sorted, guilty-until-disproven, verification log (Step 5)", "editorial-review — editor's craft gate: prose-craft (em-dash density, verbatim repetition, postulated numbers, contradictions, versal-tic) + narrative-architecture (concrete instantiation, theory-anchored hypotheses, series-title symmetry, equal action per addressee, un-overloaded conclusion), ≤10 flags BLOCK/REWORK/NICE, operator-gated via SendUserFile BEFORE the persona sweep (Step 5.5)", "persona-sweep-prelock — reader jury, primary wins, convergence to clean YES (Step 6)", diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 16c3a21..1412662 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -19,8 +19,11 @@ # S13. The external data-dir convention guard (M0: no command/agent/skill/hook prose # references a migrated user-data path in-plugin — bare or ${CLAUDE_PLUGIN_ROOT}-pinned; # + no ANALYTICS_ROOT in-plugin pin; + an SC2 dry-run that the tree holds no in-plugin -# user data) in Section 13, each with a non-vacuity self-test; the assertion-count -# anti-erosion floor (SC6) in Section 14. All are live below (Sections 8–14). +# user data) in Section 13, each with a non-vacuity self-test; the contract-gate +# binding guard (Slice 3: the §B/§C1 gate's own suite stays green and rules.ts +# ratifies 1:1 against the maskinrommet §E-manifest — KTG-only, skipped for an adopter +# shipping no deps/contract) in Section 14; the assertion-count anti-erosion floor +# (SC6) in Section 15. All are live below (Sections 8–15). # # Usage: bash scripts/test-runner.sh # bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`. @@ -573,7 +576,49 @@ fi echo "" -# --- Section 14: Assertion-Count Anti-Erosion (SC6) --- +# --- Section 14: Contract-Gate Binding (Slice 3) --- +echo "--- Contract-Gate Binding ---" + +# The deterministic §B/§C1 contract-gate (scripts/contract-gate) is wired into +# /linkedin:newsletter as Step 4.5. Two invariants belong in CI, not per-edition: +# (14a) the gate's own test suite stays green and its case count never erodes; and +# (14b) rules.ts stays bound 1:1 to the maskinrommet skrivekontrakt §E-manifest +# (ratify). Both are KTG-internal: skipped (warn, never fail) for an adopter that +# ships no contract-gate deps or no contract — so the lint stays green everywhere. +# bash 3.2-safe; `set +e` inside the test command-substitution keeps a red npm test +# from aborting the whole runner (set -e is active), and ratify exit 1 is captured by +# the `if (subshell)` form (condition context is exempt from set -e). +CG_DIR="scripts/contract-gate" +if [ -x "$CG_DIR/node_modules/.bin/tsx" ]; then + # 14a: the gate suite is green (npm test exit 0) and holds its case-count floor. + CG_OUT=$( set +e; (cd "$CG_DIR" && npm test) 2>&1; echo "CG_EXIT:$?" ) + CG_EXIT=$(echo "$CG_OUT" | grep -oE 'CG_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1) + CG_TESTS=$(echo "$CG_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1) + CONTRACT_GATE_TESTS_FLOOR=33 + if [ "$CG_EXIT" = "0" ] && [ -n "$CG_TESTS" ] && [ "$CG_TESTS" -ge "$CONTRACT_GATE_TESTS_FLOOR" ]; then + pass "contract-gate suite green: $CG_TESTS tests pass (floor $CONTRACT_GATE_TESTS_FLOOR)" + else + fail "contract-gate suite NOT green (exit=${CG_EXIT:-?}, tests=${CG_TESTS:-?}, floor $CONTRACT_GATE_TESTS_FLOOR) — run: (cd $CG_DIR && npm test)" + fi + + # 14b: ratify — rules.ts <-> §E-manifest 1:1. Skipped when the contract is absent. + CONTRACT="${MASKINROMMET_CONTRACT:-$PLUGIN_ROOT/../../maskinrommet/docs/skrivekontrakt.md}" + if [ -f "$CONTRACT" ]; then + if (cd "$CG_DIR" && node --import tsx src/cli.ts --ratify) >/dev/null 2>&1; then + pass "contract-gate ratify: rules.ts bound 1:1 to the §E-manifest (in sync)" + else + fail "contract-gate ratify DRIFT — rules.ts != §E-manifest; run: (cd $CG_DIR && npm run ratify)" + fi + else + warn "contract-gate ratify skipped — contract absent ($CONTRACT); KTG-only invariant" + fi +else + warn "contract-gate skipped — deps absent ($CG_DIR/node_modules); run: (cd $CG_DIR && npm install)" +fi + +echo "" + +# --- Section 15: Assertion-Count Anti-Erosion (SC6) --- # The lint self-modifies its own checks, so a green run could mask a silently dropped # assertion. Pin the pre-M0 total (74 pass()+fail() invocations) as a floor; the count # may only grow (brief-reviewer assumption 3). Runs last so TOTAL_CHECKS sees every prior check.