From 527391ab47155018517720100c47c7ccf62f301e Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 20 Jun 2026 22:29:01 +0200 Subject: [PATCH] feat(linkedin-studio): wire lived-specifics extraction into newsletter pipeline (fix #2 slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the kilde-så-draft binding (slices 1–2) into the live /linkedin:newsletter pipeline so a draft is grounded in the operator's real material BEFORE any prose. - New Step 1.5 "Lived-specifics extraction" BETWEEN brief (1) and research (2): query the bank → 3–5 sharp elicitation questions (vagueness refused, never fabricated — invariant 3) → `add` to the specifics-bank → populate the articles.NN.livedSpecifics slot-map → render the NN-kilder.md ledger. First phase to write machine state (persists brief + personas + binding). - Deterministic binding-gate (`validate-binding`, exit 1 = BLOCK) wired as a new step 5 of the Step 2.5 skeleton-gate (renumbered 5→9): no prose is written on an unresolved load-bearing claim; livedSpecifics → "bound" on pass. - Step 2 re-scoped BY the binding: research fills the ekstern/unresolved slots + verifies the operator's own numbers (regel 6/7) → flips bank unverified→verified → re-renders NN-kilder.md, instead of re-sourcing what is already grounded. - Pipeline 17 → 18 phases: new `lived-specifics` phase id in edition-state _doc.phases; resumption table + pipeline overview + CLAUDE.md + README updated. - Lint: new Section 15 "Specifics-Bank Binding" wires the specifics-bank suite into CI (suite green + case-count floor 28, KTG-only/adopter-skip); the assertion-count anti-erosion floor moves to Section 16. Verification: structure gate 84/0/0 (was 83), specifics-bank 28/28, contract-gate ratify 1:1, edition-state template valid JSON (18 phases), gitleaks clean on all 5 files. Design record: docs/fix2/slice2-binding.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- README.md | 4 +- commands/newsletter.md | 253 ++++++++++++++++++++++++----- config/edition-state.template.json | 3 +- scripts/test-runner.sh | 37 ++++- 5 files changed, 255 insertions(+), 44 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 809c609..ae73dba 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 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:newsletter` | Long-form orchestrator: newsletter edition / essay / series article — multi-session 18-phase pipeline with **lived-specifics extraction BEFORE research (Step 1.5, Fix #2 — kilde-så-draft; binding-gate enforced at Step 2.5)**, **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 5f4ba7d..1bfc0b4 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 **17-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: an **18-phase pipeline where the draft is grounded in your real material *before research* (lived-specifics extraction) and then 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 17-phase pipeline; all gates BEFORE lock. | +| `/linkedin:newsletter` | Long-form orchestrator — newsletter/essay/series article at series quality. Multi-session 18-phase pipeline; lived-specifics grounding BEFORE research, 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 ec3dcc0..f4f837c 100644 --- a/commands/newsletter.md +++ b/commands/newsletter.md @@ -22,7 +22,8 @@ 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 (17 phases)**: research → skeleton gate → +> a **multi-session, multi-gate process (18 phases)**: lived-specifics extraction → +> 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. @@ -82,7 +83,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 (17 phases) +## Pipeline overview (18 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 @@ -96,7 +97,8 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias). |------|-------|------|-------| | 0 | **Load context** | edition-state + `/STATE.md`, voice profile, persona library, series brief | `Read` | | 1 | **Brief + calibration** | angle, voice, audience personas (mark primær), key points, tone, leader-takeaway. ≤3 questions | `AskUserQuestion` | -| 2 | **Research** | parallel scoped mandates → verified notes; triangulation | **`Task` fan-out (foreground)** | +| 1.5 | **Lived-specifics extraction — BEFORE research** | elicitation interview (3–5 sharp questions, **vagueness refused**) → `add` lived material to the specifics-bank + populate `articles.NN.livedSpecifics` slot-map (each load-bearing key-point bound specific/abstrakt/ekstern/unresolved) → render `NN-kilder.md`. The grounded-sourcing layer (Fix #2, kilde-så-draft; never fabricated — invariant 3). | `AskUserQuestion` + `scripts/specifics-bank` (`node --import tsx src/cli.ts`) | +| 2 | **Research — scoped by the binding** | parallel scoped mandates → verified notes; triangulation. Fills the `ekstern`/`unresolved` slots + verifies your own numbers (regel 6/7) | **`Task` fan-out (foreground)** | | 2.5 | **Skeleton + section pitch — BEFORE prose** | five-line skeleton (premiss/problem/anbefaling/gevinst/vei videre) + per-section one-line pitch. Operator-gate JA/NEI/REVIDER. Persona-skjelett-sweep before any prose is written. | `AskUserQuestion` + **`persona-reviewer`** (skjelett mode) | | 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` | @@ -112,9 +114,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 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 + +> **Build status:** all 18 phases (Steps 0, 1, 1.5, 2, 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 → **lived-specifics extraction (elicitation + bank binding, +> BEFORE research)** → verified research (scoped by the binding) → **skeleton + section pitch (operator + > persona gate BEFORE prose)** → **spine prose (operator gate BEFORE full > expansion)** → full prose draft → consistency/quality → **contract-gate (deterministic §B/§C1)** → fact-check sweep → > **editorial review (craft gate, operator-gated BEFORE the persona sweep)** → @@ -206,7 +210,8 @@ Look up `edition-state.json` → `articles.` (and the top-level |---------------------------------|-----------| | *(no state file)* | **NEW edition** → Step 1 (init state at end of Step 2) | | `load-context` | Step 1 — Brief + calibration | -| `brief-calibration` | Step 2 — Research | +| `brief-calibration` | Step 1.5 — Lived-specifics extraction *(Fix #2 — elicitation + bank binding BEFORE research)* | +| `lived-specifics` | Step 2 — Research *(scoped by the binding)* | | `research` | Step 2.5 — Skeleton + section pitch *(v2.1 — skeleton gate BEFORE prose)* | | `skeleton-pitch` | Step 3a — Spine prose *(v2.1 — one paragraph per section, BEFORE full expansion)* | | `spine-prose` | Step 3b — Full prose expansion | @@ -281,7 +286,8 @@ Settle these dimensions (most should come from context, not questions): (accept it), but a *primær* NO is never accepted (revise until a clean YES). **Persist** the resolved set to `edition-state.json` → `articles.NN.personas` (each entry: name, tier, the five fields, source) at the - Step 2 checkpoint — it is then stable across sessions and is the single source + **Step 1.5 checkpoint** (the first machine-state write; Step 2 then adds the + research notes) — it is then stable across sessions and is the single source every later sweep reads. See `config/personas.template.md` → "Per-artifact personas". - **Key points** — the 2–4 load-bearing claims the edition must make. @@ -305,6 +311,128 @@ Edition brief - Leader-takeaway: ``` +## Step 1.5: Lived-specifics extraction — BEFORE research + +> **The grounded-sourcing layer (Fix #2, kilde-så-draft).** Sourcing, not styling, +> is where authenticity is won. A draft that starts from your real number / named +> case / what-actually-broke is forwardable; a draft that invents plausible filler +> is hollow, and every downstream voice gate then only polishes hollow content. So +> BEFORE research and BEFORE prose, each load-bearing claim is bound to real +> material — or you *consciously* decide it does not need a concrete instance. This +> step is that binding; **vagueness is refused** (drømme-spec G4). The plugin never +> invents lived experience (invariant 3). + +> **Order assertion (enforced).** Step 1.5 runs AFTER the brief (Step 1) and BEFORE +> research (Step 2). It populates the slot-map and renders the `NN-kilder.md` +> ledger; the BLOCKing «vaghet avvises» gate (`validate-binding`) is enforced at +> the **Step 2.5 skeleton-gate**, once the spine is visible. Research (Step 2) is +> then scoped BY this binding — it fills the `ekstern`/`unresolved` gaps and +> verifies your own numbers, instead of re-sourcing what is already grounded. `[GATE]` + +> **Adopter note.** This step uses the KTG-internal specifics-bank +> (`${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank` — run `npm install` there once). +> If its deps are absent (a plain adopter ships none), this Step is a **no-op**: +> skip to Step 2 and supply specifics inline during drafting. It never blocks an +> adopter. + +**Procedure:** + +1. **Surface inventory you already have.** Derive the edition's topic tags from the + brief (Step 1 angle + key points), and query the bank for lived material that + already matches — so the interview asks only for what is genuinely missing. The + CLI runs from its own dir (where `tsx` lives): + + ```bash + cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \ + node --import tsx src/cli.ts query --tags "" + ``` + + Show the hits to the operator; each is a candidate binding for a load-bearing + key-point. + +2. **Elicitation interview — 3–5 sharp questions, vagueness refused.** For each + load-bearing key-point (Step 1) NOT already covered by inventory, ask one tight + question that demands a *concrete instance*, not a generality. Pick the shape the + claim needs (one per question): + - a **number** you measured ("how many / how much / how long — your figure"), + - a **named case** ("which project / tool / team — name it"), + - a **what-broke** ("what actually failed, concretely"), + - a **contrarian** point ("where do you disagree with the consensus, and why"), + - a **mind-change** ("what did you used to believe and stop — what flipped it"). + + **Refuse vagueness.** If the answer is a generality ("it's generally faster", + "people struggle with this"), push back once for the concrete instance. If the + operator genuinely has none, that key-point becomes an `abstrakt` slot (with a + stated rationale) or an `ekstern` slot (research will source it) — **never a + fabricated specific** (invariant 3). The plugin elicits your raw material; it + does not invent it. + +3. **Fold elicited material into the bank.** For each concrete answer, `add` it so + it is reusable across editions (content-hash dedupe + tag-union are automatic): + + ```bash + cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \ + node --import tsx src/cli.ts add \ + --type \ + --content "" --tags "" \ + [--source ""] [--verification ] + ``` + + A figure the operator states from memory is `--verification unverified` — it + stays guilty-until-checked, and Step 2 verifies it (regel 6/7). + +4. **Populate the slot-map.** Write `articles.NN.livedSpecifics` in + `edition-state.json` — one `kind: "key-point"` slot per load-bearing claim, each + bound to exactly one of: + - `{ type: "specific", specificId: "" }` — grounded in lived material; + - `{ type: "abstrakt", rationale: "" }`; + - `{ type: "ekstern", source?: "" }` — to be + sourced, not lived; + - `{ type: "unresolved" }` — a temporary placeholder only; this is the vagueness + the Step 2.5 gate BLOCKs, so resolve it before then. + + Set `status: "pending"` (Step 2.5 flips it to `"bound"` when the gate passes). + The shape is documented in `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json` + (`_doc.livedSpecifics`). + +5. **Render the `NN-kilder.md` ledger.** Deterministically render the read-only + sources artifact from the slot-map + bank (NN = the same zero-padded edition + number as `NN-utkast.md`). Pass absolute paths: + + ```bash + cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \ + node --import tsx src/cli.ts render-kilder \ + --edition "/linkedin/edition-state.json" \ + --out "/linkedin/NN-kilder.md" + ``` + + It is regenerated like POST.html, never hand-edited. Surface it to the operator + so the binding is visible (coverage by type + the numbers-to-verify line). + +6. **Persist + checkpoint state.** Step 1.5 is the **first phase that writes machine + state**, so: + - If `edition-state.json` does not exist yet (fresh edition), initialize it from + the template schema (`${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json`). + - Persist the resolved **brief** (Step 1) and **personas** (the set + primær) + into `articles.NN` — this is the first durable write of both (Step 2 then adds + the research notes). + - 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 + to `/STATE.md` (overwrite). + +``` +Lived-specifics extraction (BEFORE research) — complete. +- Inventory hits reused: New material elicited + added to bank: +- Slot-map: slots — levd · abstrakt · ekstern · uavklart +- Numbers to verify (regel 6/7): (carried to Step 2) +- Ledger rendered: /linkedin/NN-kilder.md +- Vagueness refused: (never fabricated — invariant 3) +Next: Step 2 — Research (scoped by the binding: fill ekstern/unresolved, verify numbers). +``` + +--- + ## Step 2: Research — parallel `Task` fan-out (foreground) > **This is the load-bearing phase.** Quality long-form needs verified, triangulated @@ -313,12 +441,22 @@ Edition brief **Procedure:** -1. **Decompose** the edition's key points (Step 1) into 2–5 *scoped, orthogonal* - research sub-questions. Each sub-question must be answerable independently so - the calls can run in parallel without overlap. Reuse the multi-source synthesis - discipline from `commands/react.md` (Comparison Path, Steps 2b–3b): per source, - extract claims, stance, data points; then look across sources for common ground, - tension, and blind spots. +0. **Scope by the binding, not the raw key points (Fix #2 re-scope).** Read + `articles.NN.livedSpecifics` (Step 1.5). Research targets the **gaps the binding + names** — slots bound `ekstern` (and any still `unresolved` at entry): the claims + that explicitly need an external source. Slots bound to a `specific` are already + grounded in lived material; do **not** re-source their substance (you may + corroborate, but it is not the gap). Every **`unverified-number`** warning from + the Step 1.5 binding is also a research target: confirm the operator's own figure + against a primary source (regel 6/7). This is what makes research *fill* the + draft instead of re-deriving what the operator already knows. + +1. **Decompose** the binding's `ekstern`/`unresolved` slots + unverified numbers + (step 0) into 2–5 *scoped, orthogonal* research sub-questions. Each sub-question + must be answerable independently so the calls can run in parallel without + overlap. Reuse the multi-source synthesis discipline from `commands/react.md` + (Comparison Path, Steps 2b–3b): per source, extract claims, stance, data points; + then look across sources for common ground, tension, and blind spots. 2. **Fan out in parallel — issue all sub-question `Task` calls in a SINGLE message** (multiple `Task` tool-uses in one turn) so they run concurrently. Each call gets @@ -353,21 +491,27 @@ Edition brief source(s) and a confidence marker. Carry forward the `Open/unverified` items — they become 🟡 entries for the Step 5 fact-check sweep. -5. **Persist + checkpoint state.** Write the resolved brief (Step 1), the - resolved **per-article personas** (`articles.NN.personas` — the set + primær - confirmed/defined in Step 1), and the verified research notes into the - edition's `edition-state.json` (`currentPhase: "research"`, article status - `in-progress`) and append a +5. **Persist + checkpoint state.** Write the verified research notes into + `edition-state.json` (the brief + personas were first persisted at the Step 1.5 + checkpoint — re-affirm them here if calibration changed). **Update the binding + from what research resolved:** when a sub-question sources an `ekstern` slot, set + its `source`; when it confirms an operator figure, flip the bank specific + `unverified → verified` (the bank's `add`/verify path), then **re-render + `NN-kilder.md`** so the ledger stays current (a number that could not be verified + stays `unverified` and is carried to the Step 5 fact-check sweep as 🟡 — never + silently asserted). Set `currentPhase: "research"`, article status + `in-progress`, and append a "research complete → next: skeleton + section pitch (BEFORE prose)" next-step - line to `/STATE.md` (overwrite). If this is a fresh edition, initialize - `edition-state.json` from the template schema first. Stop cleanly here if - context budget is tight — Step 2.5 begins in the next session; otherwise - Step 2.5 may run inline (it is short and operator-interactive). + line to `/STATE.md` (overwrite). If somehow no `edition-state.json` exists + yet (Step 1.5 was a no-op for an adopter), initialize it from the template schema + first. Stop cleanly here if context budget is tight — Step 2.5 begins in the next + session; otherwise Step 2.5 may run inline (it is short and operator-interactive). ``` Research phase complete. -- Sub-questions: (ran in parallel) +- Sub-questions: (ran in parallel; scoped to ekstern/unresolved slots + numbers) - Verified notes: by key point +- Binding updated: ekstern slots sourced · numbers verified → NN-kilder.md re-rendered - Carried to fact-check (🟡 unverified): State written: /linkedin/edition-state.json (phase: research) Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE prose). @@ -386,10 +530,11 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros > **Order assertion (enforced).** Step 2.5 runs AFTER research (Step 2) and > BEFORE any prose (Step 3a). No section of the draft is written — not even -> spine prose — until the operator says JA on the skeleton and the +> spine prose — until the operator says JA on the skeleton, the **deterministic +> binding-gate** passes (every load-bearing claim grounded — Fix #2), and the > persona-skjelett-sweep returns a clean primær JA. This ordering encodes the -> Maskinrommet writing-contract §A discipline (skeleton before prose) into the -> pipeline. `[GATE]` +> Maskinrommet writing-contract §A discipline (skeleton + sources before prose) +> into the pipeline. `[GATE]` **Procedure:** @@ -454,17 +599,47 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros JA proceed, or another round?». `AskUserQuestion` (JA / REVIDER / NEI) is also the **fallback** gate when rendering is unavailable; it is not the primary flow: - - **JA** — proceed to the persona-skjelett-sweep (step 5). + - **JA** — proceed to the binding-gate (step 5), then the + persona-skjelett-sweep (step 6). - **REVIDER** — another annotation round; revise and re-render. - **NEI** — the skeleton is wrong at a load-bearing level (premise unsound, argument-line incoherent). Return to brief calibration (Step 1) or research (Step 2) to surface the missing piece. Do not proceed past this gate without an explicit JA. The pipeline may not - advance to Step 3a (spine prose) until both this operator-gate AND the - persona-skjelett-sweep below return JA. `[OPERATØR]` + advance to Step 3a (spine prose) until this operator-gate, the deterministic + binding-gate, AND the persona-skjelett-sweep below all pass. `[OPERATØR]` -5. **Persona-skjelett-sweep — fan out `persona-reviewer` in skjelett-mode.** +5. **Binding-gate — deterministic «vaghet avvises» (Fix #2, BEFORE the persona + sweep).** Now that the spine is visible, enforce the Step 1.5 binding: every + load-bearing slot must resolve to a real specific, a justified `abstrakt`, or an + `ekstern` source — no `unresolved` slot, no `specific` pointing at a missing bank + id. It is pure/deterministic (no model in the loop), so the same vagueness is + caught the same way every time. Run it from the package dir; pass the edition + state's **absolute** path: + + ```bash + cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \ + node --import tsx src/cli.ts validate-binding \ + --edition "/linkedin/edition-state.json" + ``` + + - **exit 0** → PASS. Surface any **WARN** (`unverified-number`, regel 6/7) as + direction — a number still unverified after Step 2 research is carried to the + Step 5 fact-check sweep as 🟡, never silently asserted. + - **exit 1** → BLOCKED. The output names each blocking slot (`unresolved` / + `dangling-specific` / `unjustified-abstrakt`). Resolve it — elicit the missing + specific (loop back to Step 1.5 step 2), fix the dangling id, or give the + `abstrakt` escape a real rationale — then **re-render `NN-kilder.md`** and + re-run until exit 0. No prose is written on an unresolved load-bearing + claim. `[GATE]` + - deps/contract absent (adopter — Step 1.5 was a no-op) → skip with a one-line + note; the persona-skjelett-sweep below still runs. + + When the gate passes, set `articles.NN.livedSpecifics.status: "bound"` in + `edition-state.json`. + +6. **Persona-skjelett-sweep — fan out `persona-reviewer` in skjelett-mode.** Issue one `persona-reviewer` call per active persona in parallel — a SINGLE message with multiple `Task` tool-uses, `subagent_type: linkedin-studio:persona-reviewer`, from THIS command layer in @@ -474,7 +649,7 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros This is NOT resonans mode (Step 6 — that runs on full prose) and NOT konverter mode (Step 9 — that judges the hook only). -6. **Collect skjelett verdicts and gate.** Each call returns per-axis flags +7. **Collect skjelett verdicts and gate.** Each call returns per-axis flags (HOLDER/TVILER/MANGLER), ≤3 direction-only flags, a section-pitch check (any pitch that does not pay in), a per-persona verdict (JA/NEI), and a gate decision. Aggregate per the agent's rule: @@ -493,17 +668,19 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros the editor (this session) holds the pen; never paste a persona's rewritten skeleton. `[GATE]` -7. **Convergence loop.** If gate is REWORK/BLOCK, fold flags into the +8. **Convergence loop.** If gate is REWORK/BLOCK, fold flags into the skeleton + pitches (or, on BLOCK, return upstream) and re-run the same `persona-reviewer` calls against the revision. Loop until the primær returns a clean JA. This loop is **cheap and frequent at this stage** — every round saved here is hours saved at the prose stage. -8. **Persist + checkpoint state.** Once the skeleton is JA from both operator - AND persona-skjelett-sweep, record: +9. **Persist + checkpoint state.** Once the skeleton is JA from the operator, the + binding-gate passed, AND the persona-skjelett-sweep returns a clean primær JA, + record: - The final skeleton + pitches in `/NN-skjelett.md` (already written in step 3, with any in-loop revisions applied). + - `articles.NN.livedSpecifics.status: "bound"` (set by the binding-gate, step 5). - Per-persona skjelett verdicts in `edition-state.json` → `articles.NN.personaSweep.skeleton` (or alongside resonance/conversion under the same `personaSweep` object). @@ -517,10 +694,11 @@ Skeleton + section pitch (BEFORE prose) — complete. - Skeleton: 5 lines (premiss / problem / anbefaling / gevinst / vei videre) - Section pitches: sections, all paying into the spine (else: pitches reworked, see flags) - Operator gate: JA (after revision rounds) +- Binding-gate (vaghet avvises): PASS — all load-bearing slots bound; livedSpecifics → "bound" (else BLOCK: unresolved/dangling) - Persona-skjelett-sweep: primær JA (else: still NEI — loop open, NOT ready for prose) - Convergence rounds: - Accepted sekundær ceiling-NOs (signal, not failure): -Gate: [PASS — primær JA, ready for spine prose] (else REWORK/BLOCK) +Gate: [PASS — primær JA + binding bound, ready for spine prose] (else REWORK/BLOCK) Next: Step 3a — Spine prose (one paragraph per section, BEFORE full expansion). ``` @@ -1642,7 +1820,8 @@ the honest decision surface; it sells nothing. ## Reference Files -- `${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-state.template.json` — edition-state schema (18 phases including Fix #2 lived-specifics extraction (Step 1.5) + `articles.NN.livedSpecifics`, 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}/scripts/specifics-bank/` — lived-specifics store + per-edition binding (Fix #2, kilde-så-draft): `query`/`add` (Step 1.5), `validate-binding` (Step 2.5 gate), `render-kilder` (`NN-kilder.md` ledger). Design record: `docs/fix2/slice2-binding.md` - `${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 584cc7e..1db186e 100644 --- a/config/edition-state.template.json +++ b/config/edition-state.template.json @@ -7,7 +7,8 @@ "phases": [ "load-context — read /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)", + "lived-specifics — elicitation interview (3–5 sharp questions, vagueness refused) → add lived material to the specifics-bank + populate articles.NN.livedSpecifics slot-map (each load-bearing key-point bound specific/abstrakt/ekstern/unresolved) → render NN-kilder.md; BETWEEN brief (Step 1) and research (Step 2). The grounded-sourcing layer (Fix #2, kilde-så-draft; never fabricated — invariant 3). The BLOCKing «vaghet avvises» gate (validateBinding) is enforced at the Step 2.5 skeleton-gate (Step 1.5)", + "research — parallel scoped mandates → verified notes, triangulation; scoped BY the binding (fills ekstern/unresolved slots + verifies the operator's own numbers, regel 6/7) (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)", diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 1412662..3c76fce 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -22,8 +22,11 @@ # 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). +# shipping no deps/contract) in Section 14; the specifics-bank binding guard (Fix #2 +# slice 3: the lived-specifics store/binding suite stays green and its case count +# never erodes — KTG-only, skipped for an adopter shipping no deps) in Section 15; +# the assertion-count anti-erosion floor (SC6) in Section 16. All are live below +# (Sections 8–16). # # Usage: bash scripts/test-runner.sh # bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`. @@ -618,7 +621,35 @@ fi echo "" -# --- Section 15: Assertion-Count Anti-Erosion (SC6) --- +# --- Section 15: Specifics-Bank Binding (Fix #2 slice 3) --- +echo "--- Specifics-Bank Binding ---" + +# The lived-specifics store + per-edition binding (scripts/specifics-bank) is wired +# into /linkedin:newsletter as Step 1.5 (elicitation + slot-map) and its +# validate-binding gate into the Step 2.5 skeleton-gate. The seam is now load-bearing, +# so its suite belongs in CI: the bank/binding/kilder tests stay green and the case +# count never erodes. KTG-internal: skipped (warn, never fail) for an adopter that +# ships no specifics-bank deps — so the lint stays green everywhere. Same set +e / +# subshell discipline as Section 14 (bash 3.2-safe; keeps a red npm test from +# aborting the runner under set -e). +SB_DIR="scripts/specifics-bank" +if [ -x "$SB_DIR/node_modules/.bin/tsx" ]; then + SB_OUT=$( set +e; (cd "$SB_DIR" && npm test) 2>&1; echo "SB_EXIT:$?" ) + SB_EXIT=$(echo "$SB_OUT" | grep -oE 'SB_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1) + SB_TESTS=$(echo "$SB_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1) + SPECIFICS_BANK_TESTS_FLOOR=28 + if [ "$SB_EXIT" = "0" ] && [ -n "$SB_TESTS" ] && [ "$SB_TESTS" -ge "$SPECIFICS_BANK_TESTS_FLOOR" ]; then + pass "specifics-bank suite green: $SB_TESTS tests pass (floor $SPECIFICS_BANK_TESTS_FLOOR)" + else + fail "specifics-bank suite NOT green (exit=${SB_EXIT:-?}, tests=${SB_TESTS:-?}, floor $SPECIFICS_BANK_TESTS_FLOOR) — run: (cd $SB_DIR && npm test)" + fi +else + warn "specifics-bank skipped — deps absent ($SB_DIR/node_modules); run: (cd $SB_DIR && npm install)" +fi + +echo "" + +# --- Section 16: 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.