feat(linkedin-studio): N11 — serie-destillat + skjelett-sjekk + specifics-bank bruks-logging [skip-docs]
Every long-form gate agent sees ONE edition, so «the reader has heard this
before» was structurally invisible — and the specifics-bank dedupe actively
encouraged re-surfacing the same material. At series cadence that is the
fastest-growing defect class. N11 makes it visible BEFORE the skeleton is
approved, at two grains:
- Series grain — new scripts/editions package: each locked edition's spent
anecdotes/arguments/hooks are written to <serie>/linkedin/series-distillate.json
at Step 8 lock (distil-append), and the next skeleton is checked against it at
Step 2.5 (distil-check) with the finding folded into the annotation gate the
operator already reads. Advisory, never blocking: a deliberate callback is a
legitimate move, an unnoticed retread is not.
- Material grain — specifics-bank usedIn log: record-usage stamps «used in
edition NN» on the specifics an edition actually consumed (read from the bound
slot-map, so abstrakt/ekstern stamp nothing). At lock, so it means published;
idempotent under a pivot re-lock. Additive-optional, schema stays v1.
Placement deviates from the plan text (${DATA}) after premise-verification:
per-series state belongs in the series root beside edition-state.json, where
Step 0 already resolves the path and no slug→path map is needed. Operator
approved. The distillate module also lands in scripts/editions now rather than
at N12, since the AC required a testable roundtrip and N12 planned that package
anyway — N12 extends it instead of creating it.
Similarity is character-trigram Jaccard, not word overlap: the plugin is
language-general and inflection (migrere/migreringen) breaks word tokens.
Calibrated on real paraphrase pairs — retellings 0.44-0.55, unrelated 0.04-0.06,
same-topic-different-story 0.24 — so the default threshold sits in the gap at
0.40. Word-Jaccard scored a shortened hook paraphrase at 0.11.
TDD (Iron Law): tests written first and verified red before implementation, for
both the new package and the bank logging.
Suites: editions 27/0 (new suite line + guard, floor 27) · specifics-bank
28 -> 45 · test-runner 163 -> 173 (Section 16r: 9 unconditional greps +
non-vacuity self-test; anti-erosion floor 146 -> 155) · trends 300/0 ·
brain 134/0 · hooks 140/0 · tests 35/0 · render 60/0. tsc --noEmit clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
parent
08106317db
commit
b54e450c3e
20 changed files with 2152 additions and 19 deletions
|
|
@ -99,7 +99,7 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
|
|||
| 1 | **Brief + calibration** | angle, voice, audience personas (mark primær), key points, tone, leader-takeaway. ≤3 questions | `AskUserQuestion` |
|
||||
| 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) |
|
||||
| 2.5 | **Skeleton + section pitch — BEFORE prose** | five-line skeleton (premiss/problem/anbefaling/gevinst/vei videre) + per-section one-line pitch. **Serie-destillat-sjekk** (has the reader heard this anecdote/argument/hook before? advisory, into the annotation gate). Operator-gate JA/NEI/REVIDER. Persona-skjelett-sweep before any prose is written. | `AskUserQuestion` + `scripts/editions` (`distil-check`) + **`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` |
|
||||
| 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`** |
|
||||
|
|
@ -110,7 +110,7 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
|
|||
| 6.5 | **Headless adversarial review — BEFORE lock** | COLD review package on a frozen draft, no drafting-session context: content-reviewer (argument) + language-reviewer (Norwegian) + fact-reviewer (cold re-verification incl. pivot premises) + persona-reviewer resonance/conversion. Consolidated, operator-gated via `SendUserFile`. The independence layer the in-session gates can't be. | **`content-reviewer` + `language-reviewer` + `fact-reviewer` + `persona-reviewer`** (parallel) + `SendUserFile` |
|
||||
| 7 | **Annotation (optional)** | render annotatable review HTML for a manual pass | `render/build-html.mjs` |
|
||||
| 7.5 | **Visual assets — BEFORE lock** | cover (+ optional inline figures) or carousel deck: behov → per-image brief → generate (coded `build-figur.mjs` primary for data figures / mcp-image for illustrative / external `cover-raw.png`) → operator-gate (`SendUserFile`) → approve to `cover.png` → credit/caption. Runs before lock so the renderer picks the cover up. | `render/build-figur.mjs` (data figures) + `mcp__mcp-image__generate_image` + `SendUserFile` + (carousel) `render/build-carousel.mjs` |
|
||||
| 8 | **LOCK → delivery** | POST.html "all in one place" | `render/build-linkedin.mjs` |
|
||||
| 8 | **LOCK → delivery** | POST.html "all in one place"; auto-gull (gold corpus) + **serie-minne** (series distillate + specifics-bank usage log) | `render/build-linkedin.mjs` + `scripts/editions` (`distil-append`) + `scripts/specifics-bank` (`record-usage`) |
|
||||
| 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` |
|
||||
|
||||
|
|
@ -666,7 +666,36 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
|
|||
2. …
|
||||
```
|
||||
|
||||
4. **Operator-gate (render + annotate — primary flow).** The operator review is
|
||||
4. **Serie-destillat-sjekk — «har leseren hørt dette før?» (deterministic).**
|
||||
Every other gate in this pipeline sees ONE edition. At series cadence the
|
||||
fastest-growing defect is not a bad edition but a **repeated** one — the
|
||||
anecdote, the argument or the hook the reader already met three editions ago.
|
||||
Check the proposed skeleton against everything the series has locked, while
|
||||
changing course is still cheap. Write the units the spine + pitches actually
|
||||
propose to a small JSON file (`{anecdotes, arguments, hooks}` — the operator's
|
||||
phrasing, not a summary), then run it from the package dir with **absolute**
|
||||
paths:
|
||||
|
||||
```bash
|
||||
cd "${CLAUDE_PLUGIN_ROOT}/scripts/editions" && \
|
||||
node --import tsx src/cli.ts distil-check \
|
||||
--distillate "<abs>/linkedin/series-distillate.json" \
|
||||
--skeleton "<abs>/skjelett-units.json"
|
||||
```
|
||||
|
||||
- **CLEAR** → nothing here echoes an earlier edition. Carry the one-liner into
|
||||
the annotation page as a receipt.
|
||||
- **REUSE** → the output names each unit, the edition it echoes, and how close
|
||||
it is. This is **advisory, never a block** (exit 0 either way): a deliberate
|
||||
callback is a legitimate move — an *unnoticed* retread is not. Fold the
|
||||
finding into the annotation page (step 5) so the operator decides with the
|
||||
re-use in view: keep it, sharpen it into an explicit callback, or replace
|
||||
the unit.
|
||||
- No distillate yet (a series' first edition, or an adopter who has not locked
|
||||
one since this gate existed) → CLEAR against 0 editions; say so and move on.
|
||||
Deps absent → skip with a one-line note, like the binding-gate.
|
||||
|
||||
5. **Operator-gate (render + annotate — primary flow).** The operator review is
|
||||
**HTML annotation**, not a multiple-choice prompt — and this holds for *every*
|
||||
write deliverable, the skeleton included, not just the final POST.html. Render
|
||||
the skeleton to an annotatable page and let the operator annotate in the browser:
|
||||
|
|
@ -680,7 +709,9 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
|
|||
`<serie-mappe>/review/NN-skjelett.html` exists before handing it off.
|
||||
3. Surface `<serie-mappe>/review/NN-skjelett.html` to the operator as a
|
||||
`file://` link (use `SendUserFile` if available, else a markdown `file://`
|
||||
link) for the annotation pass.
|
||||
link) for the annotation pass. If the destillat-sjekk (step 4) returned
|
||||
**REUSE**, state each echoed unit + the edition it came from alongside the
|
||||
link — the operator annotates with the re-use visible, not after the fact.
|
||||
4. The operator annotates in the browser and pastes the annotated markdown
|
||||
back. Fold the notes into `<serie>/NN-skjelett.md` by tightening (rule 6).
|
||||
5. **Receipt, not gate.** After folding in the annotations, use
|
||||
|
|
@ -688,8 +719,8 @@ 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 binding-gate (step 5), then the
|
||||
persona-skjelett-sweep (step 6).
|
||||
- **JA** — proceed to the binding-gate (step 6), then the
|
||||
persona-skjelett-sweep (step 7).
|
||||
- **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
|
||||
|
|
@ -699,7 +730,7 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
|
|||
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. **Binding-gate — deterministic «vaghet avvises» (Fix #2, BEFORE the persona
|
||||
6. **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
|
||||
|
|
@ -728,7 +759,7 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
|
|||
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.**
|
||||
7. **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
|
||||
|
|
@ -738,7 +769,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).
|
||||
|
||||
7. **Collect skjelett verdicts and gate.** Each call returns per-axis flags
|
||||
8. **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:
|
||||
|
|
@ -757,19 +788,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]`
|
||||
|
||||
8. **Convergence loop.** If gate is REWORK/BLOCK, fold flags into the
|
||||
9. **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.
|
||||
|
||||
9. **Persist + checkpoint state.** Once the skeleton is JA from the operator, the
|
||||
10. **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 `<serie>/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).
|
||||
- `articles.NN.livedSpecifics.status: "bound"` (set by the binding-gate, step 6).
|
||||
- Per-persona skjelett verdicts in
|
||||
`edition-state.json` → `articles.NN.personaSweep.skeleton` (or alongside
|
||||
resonance/conversion under the same `personaSweep` object).
|
||||
|
|
@ -782,6 +813,7 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
|
|||
Skeleton + section pitch (BEFORE prose) — complete.
|
||||
- Skeleton: 5 lines (premiss / problem / anbefaling / gevinst / vei videre)
|
||||
- Section pitches: <N> sections, all paying into the spine (else: pitches reworked, see flags)
|
||||
- Series distillate: CLEAR against <N> locked edition(s) (else REUSE: <N> unit(s) — kept as explicit callback / replaced)
|
||||
- Operator gate: JA (after <N> revision rounds)
|
||||
- Binding-gate (vaghet avvises): PASS — all load-bearing slots bound; livedSpecifics → "bound" (else BLOCK: <N> unresolved/dangling)
|
||||
- Persona-skjelett-sweep: primær JA (else: still NEI — loop open, NOT ready for prose)
|
||||
|
|
@ -1797,12 +1829,56 @@ produces the editor's single delivery artifact — `POST.html`, the
|
|||
one edition at a time — never the engine's drafts. The next edition's Step 0
|
||||
therefore binds to a sharper profile without any manual corpus curation.
|
||||
|
||||
6. **Serie-minne — record what this edition spent.** The reader-facing twin of
|
||||
auto-gull: auto-gull grows the *voice* corpus, this grows the *series memory*
|
||||
that the next edition's Step 2.5 destillat-sjekk is checked against. Both
|
||||
writes happen at LOCK, so they mean **published**, never merely considered —
|
||||
an abandoned draft leaves no trace.
|
||||
|
||||
- **Series distillate.** Extract the narrative units this edition actually
|
||||
spent — the anecdotes told, the arguments argued, the hooks used — **in the
|
||||
operator's own phrasing**, not as a summary (the check compares text, so a
|
||||
paraphrased summary would compare the summariser's words, not the reader's
|
||||
experience). Write them to a JSON extract and fold it in:
|
||||
|
||||
```bash
|
||||
cd "${CLAUDE_PLUGIN_ROOT}/scripts/editions" && \
|
||||
node --import tsx src/cli.ts distil-append \
|
||||
--distillate "<abs>/linkedin/series-distillate.json" \
|
||||
--series "<slug>" --extract "<abs>/NN-destillat.json"
|
||||
```
|
||||
|
||||
Extract shape: `{editionId, title, lockedAt, anecdotes[], arguments[],
|
||||
hooks[]}`. A re-lock after `/linkedin:pivot` **replaces** that edition's
|
||||
entry rather than duplicating it — the reader still only met it once.
|
||||
|
||||
- **Specifics-bank usage log.** Stamp the lived material this edition consumed
|
||||
with «used in edition NN», so the next Step 1.5 query surfaces re-use
|
||||
instead of silently encouraging it (the bank's dedupe unions tags across
|
||||
editions by design — without this log, re-surfacing the same specific is
|
||||
invisible):
|
||||
|
||||
```bash
|
||||
cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
|
||||
node --import tsx src/cli.ts record-usage \
|
||||
--edition "<abs>/linkedin/edition-state.json" --edition-id "<slug>/NN"
|
||||
```
|
||||
|
||||
The ids are read from the article's bound slot-map, so only material the
|
||||
edition actually leaned on is stamped; `abstrakt`/`ekstern` slots consume no
|
||||
bank material. Idempotent — a pivot re-lock does not double-count.
|
||||
|
||||
- Deps/contract absent (adopter — Step 1.5 was a no-op, or `npm install` never
|
||||
run) → skip both with a one-line note, like the Step 2.5 gates. The lock
|
||||
itself never depends on this bookkeeping.
|
||||
|
||||
```
|
||||
LOCK → delivery.
|
||||
- Preconditions: factcheck 🔴 = none, persona resonans primær = JA (else: STOP)
|
||||
- Delivered: <serie-mappe>/linkedin/NN/POST.html
|
||||
- build-linkedin exit: 0 + POST.html present (else: non-zero / skip — NOT locked)
|
||||
- Article status: locked
|
||||
- Serie-minne: distillate +1 edition (<N> units) · bank usage stamped on <N> specific(s) (else: skipped — deps absent)
|
||||
Next: Step 9 — Hook / conversion gate (post-lock).
|
||||
```
|
||||
|
||||
|
|
@ -2041,7 +2117,8 @@ the honest decision surface; it sells nothing.
|
|||
## Reference Files
|
||||
|
||||
- `${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}/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), `record-usage` (Step 8 lock — «used in edition NN»). Design record: `docs/fix2/slice2-binding.md`
|
||||
- `${CLAUDE_PLUGIN_ROOT}/scripts/editions/` — series-level memory (N11): `distil-append` writes what a locked edition spent to `<serie>/linkedin/series-distillate.json` (Step 8), `distil-check` gates the next skeleton against it (Step 2.5). Deterministic character-trigram similarity; advisory, never blocking
|
||||
- `${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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue