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) <noreply@anthropic.com>
90 lines
12 KiB
JSON
90 lines
12 KiB
JSON
{
|
||
"_doc": {
|
||
"purpose": "Schema for edition-state.json — deterministic resumption state for a newsletter edition in production. Holds the current phase + per-article status so /linkedin:newsletter (Step 0) can resume exactly where a prior session stopped.",
|
||
"decision": "G — production state lives in the series folder (e.g. $LTL_SERIES_ROOT/<slug>/linkedin/edition-state.json, default $HOME/linkedin-series/<slug>/...), NOT in the plugin. This file is the schema-defining TEMPLATE only; copy + fill it in the series folder when producing an edition.",
|
||
"complements": "edition-config.json (static: calendar, freshness, captions) and <serie>/STATE.md (human-readable narrative state, overwritten each phase per the ONE-system continuity rule — there is no edition-HANDOVER.md). edition-state.json is the machine-readable companion: deterministic resumption + the durable fact-check log, immutable rules, and persona verdicts that the old HANDOVER §4/§5 used to carry.",
|
||
"lifecycle": "/linkedin:newsletter reads this in Step 0 and rewrites it at every phase transition. Article keys mirror edition-config.json (zero-padded strings: \"01\", \"02\", ..., or \"samle\").",
|
||
"phases": [
|
||
"load-context — read <serie>/STATE.md, voice profile, persona library, series brief (Step 0)",
|
||
"brief-calibration — angle, voice, audience personas, key points, leader-takeaway (Step 1)",
|
||
"research — parallel scoped mandates → verified notes, triangulation (Step 2)",
|
||
"skeleton-pitch — five-line skeleton (premise/problem/recommendation/payoff/forward) + section pitches, operator gate + persona-skjelett-sweep BEFORE prose (Step 2.5)",
|
||
"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)",
|
||
"headless-review — adversarial review package run COLD on a FROZEN draft (no drafting-session context): content-reviewer (argument integrity) + language-reviewer (Norwegian language) + fact-reviewer (cold re-verification incl. pivot premises) + persona-reviewer in resonance & conversion modes. Fan-out from Step 6.5 or the standalone /linkedin:headless-review command; consolidated report operator-gated via SendUserFile BEFORE lock (Step 6.5)",
|
||
"annotation — optional annotatable review HTML for a manual pass (Step 7)",
|
||
"visual-assets — cover (+ optional inline figures) or carousel deck: brief → generate → operator-gate → approve, BEFORE lock so build-linkedin.mjs picks them up (Step 7.5)",
|
||
"lock-delivery — LOCK → POST.html all-in-one-place deliverable (Step 8)",
|
||
"hook-conversion-gate — persona gate on distribution text post-lock: would YOU click? (Step 9)",
|
||
"scheduling — register edition in plugin queue/state for native LinkedIn scheduling (Step 10)"
|
||
],
|
||
"articleStatusValues": ["pending", "in-progress", "locked", "scheduled"],
|
||
"editorialReview": "Per-article editorial-review record written by Step 5.5 (editorial-review phase). Runs AFTER fact-check (Step 5) and BEFORE the persona sweep (Step 6): the editorial-reviewer agent judges CRAFT (prose-craft + narrative-architecture), not reader-response, mirroring the Maskinrommet skrivekontrakt §C2. The report (≤10 flags, each with kategori P1–P5/A1–A5, quote/line-ref, direction, severity BLOCK/REWORK/NICE) is surfaced to the operator via SendUserFile; the operator decides which flags fold in. Shape: { reportPath, flagCount, byAxis: { prosa, arkitektur }, bySeverity: { block, rework, nice }, foldedIn, waived, status }. status ladder: pending → reviewed → folded. null until Step 5.5 runs. This is the craft companion to factcheckLog (truth) and personaSweep (response).",
|
||
"visualAssets": "Per-article visual-asset record written by Step 7.5 (visual-assets phase). Runs BEFORE lock because render/build-linkedin.mjs picks up linkedin/NN/cover.png + the edition-config credit/caption when it builds POST.html — generating images after lock would force a re-render. Shape: { format: \"standard\" | \"carousel\"; cover: { brief, route, candidates[], approved, status }; figures: [ { id, brief, placement, status } ]; carousel: null | { source, pdf, status } }. format \"standard\" = cover + optional inline figures (cover.png is mandatory per the KTG cover-directive); format \"carousel\" = typografisk deck via render/build-carousel.mjs instead of cover+inline (cover/figures stay empty). route: \"mcp-image\" (default, via mcp__mcp-image__generate_image) | \"external\" (DALL·E / Midjourney / photographer → linkedin/NN/cover-raw.png). status ladder: pending → briefed → generated → approved. candidates[] holds the cover-v<N>-kandidat.png attempts; approved is the fixed approved name (\"cover.png\") once the operator-gate passes. figures[].id = \"fig1\"..; placement = section reference in NN-utkast.md (figures are referenced in the draft via  and uploaded manually in the LinkedIn editor — build-linkedin.mjs does NOT embed them). Naming convention: cover.png (approved, fixed — what build-linkedin.mjs reads) | cover-v<N>-kandidat.png (attempts) | cover-raw.png (optional external pre-edit source) | fig<N>.png (inline). credit + caption are recorded in <serie>/linkedin/image-credit-caption.md and flow into edition-config.json coverCredit + captions[NN].",
|
||
"personas": "Per-article resolved reader-persona set (input config), written/confirmed in Step 1. This makes personas configurable PER ARTIFACT, not just from one global plugin library: Step 1 resolves them in order — (1) already present here → use as-is; (2) <serie>/linkedin/personas.md (per-series file) → load; (3) plugin config/personas.local.md (or personas.template.md) library → select a subset; (4) none/insufficient → DEFINE interactively via AskUserQuestion. Exactly one entry has tier \"primær\" (the rest \"sekundær\"); «primær trumfer» on conflict. This set feeds BOTH the in-session sweep (Step 6) and the headless package (Step 6.5 / persona-reviewer). Each entry: { name, tier: \"primær\" | \"sekundær\", rolle, avkobler, overbeviser, ekspertise, sjargong, source: \"edition-state\" | \"series-file\" | \"plugin-library\" | \"interactive\" }. Default []: resolved on first Step 1.",
|
||
"headlessReview": "Per-article headless-review record written by Step 6.5 (headless-review phase). Runs AFTER the in-session persona sweep (Step 6) and BEFORE lock (Step 8), on a FROZEN snapshot of the publish-ready (or pivoted) draft, fanned out from the command layer (foreground) or invoked standalone via /linkedin:headless-review in a fresh/cold session. Five archetypes judge independently with NO drafting-session context: content-reviewer (argument integrity), language-reviewer (Norwegian language), fact-reviewer (cold re-verification incl. claims a late pivot bolted on), persona-reviewer mode=resonans (per active persona), persona-reviewer mode=konverter (primær, hook only). The consolidated report is surfaced to the operator via SendUserFile; the operator decides which flags fold in. Shape: { frozenDraft, reviewers: { content, language, fact, personaResonance, personaConversion } (each { reportPath, summary, status }), consolidatedReport, foldedIn, waived, status }. status ladder: pending → run → folded. null until Step 6.5 runs. This is the adversarial-independence companion to the in-session gates (editorialReview, personaSweep, factcheckLog) — deliberately redundant: a cold reader catches what the framing-biased in-session pass missed.",
|
||
"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 5–6.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."
|
||
},
|
||
"schemaVersion": 1,
|
||
"series": {
|
||
"slug": "<series-slug>",
|
||
"title": "<Series title>"
|
||
},
|
||
"language": "en",
|
||
"currentArticle": "01",
|
||
"currentPhase": "load-context",
|
||
"updatedAt": "<ISO-8601 timestamp>",
|
||
"articles": {
|
||
"01": {
|
||
"title": "<Article 1 title>",
|
||
"phase": "load-context",
|
||
"status": "pending",
|
||
"personas": [],
|
||
"immutableRules": null,
|
||
"factcheckLog": null,
|
||
"editorialReview": null,
|
||
"personaSweep": {
|
||
"skeleton": null,
|
||
"resonance": null,
|
||
"conversion": null
|
||
},
|
||
"headlessReview": {
|
||
"frozenDraft": null,
|
||
"reviewers": {
|
||
"content": null,
|
||
"language": null,
|
||
"fact": null,
|
||
"personaResonance": null,
|
||
"personaConversion": null
|
||
},
|
||
"consolidatedReport": null,
|
||
"foldedIn": null,
|
||
"waived": null,
|
||
"status": "pending"
|
||
},
|
||
"visualAssets": {
|
||
"format": "standard",
|
||
"cover": {
|
||
"brief": null,
|
||
"route": null,
|
||
"candidates": [],
|
||
"approved": null,
|
||
"status": "pending"
|
||
},
|
||
"figures": [],
|
||
"carousel": null
|
||
},
|
||
"pivots": [],
|
||
"foldIns": [],
|
||
"locked": false,
|
||
"scheduled": null
|
||
}
|
||
}
|
||
}
|