# editions — series-level memory for long-form Two stores, one package, both about what a *series* knows rather than what an edition knows: the **distillate** (N11) — what the reader has already been told — and the **register** (N12) — what is in production right now and how long it is taking. ## 1. Series distillate (N11, serie-nivå-vern) Every long-form gate agent sees **one** edition. At two editions a week the fastest- growing defect class is not a bad edition — it is a **repeated** one: the anecdote, the argument or the hook the reader already met three editions ago. That failure is structurally invisible today, and the specifics-bank dedupe actively *encourages* re-surfacing the same material. So each locked edition leaves behind a **distillate** of the narrative units it spent, and the Step 2.5 skeleton gate checks the next skeleton against it — **before prose exists**, the only point where changing course is still cheap. ## Where the distillate lives `/linkedin/series-distillate.json` — beside `edition-state.json` in the **series root**, not in the per-user data dir. It is series-scoped state, the series folder is already where the edition's state lives, and putting it there means it travels with the series and needs no slug→path map. Series-scoped is the point: this answers *"has this reader heard it?"*. Cross-series re-use of the operator's raw material is a different grain, tracked by the specifics-bank's `usedIn` log. ## Setup ```bash cd scripts/editions && npm install ``` ## Use ```bash # Step 8 (lock): fold the edition's spent units into the series distillate node --import tsx src/cli.ts distil-append \ --distillate "/linkedin/series-distillate.json" \ --series seres --extract "/extract.json" # Step 2.5 (before prose): check the proposed skeleton against everything published node --import tsx src/cli.ts distil-check \ --distillate "/linkedin/series-distillate.json" \ --skeleton "/skeleton.json" [--threshold 0.4] [--json] ``` `extract.json` = `{editionId, title, lockedAt, anecdotes[], arguments[], hooks[]}` — the AI extract from the locked edition. `skeleton.json` = `{anecdotes?, arguments?, hooks?}`. Exit codes: `0` on success — **including a REUSE verdict**. The check is advisory by design (it reports into the annotation gate the operator already reads at Step 2.5), unlike the binding gate's BLOCK. `2` on usage error. ## Model - **Extraction is AI, comparison is code.** The command layer distils the units from the locked prose; the store, the similarity and the verdict are plain deterministic code — no model in the loop, no run-to-run variance. - **Similarity = character-trigram Jaccard**, not word overlap. The plugin is language-general and the languages it is used in are inflection-heavy: *migrere* / *migreringen* are the same unit to a reader but different word tokens. Calibrated against real paraphrase pairs (see `tests/distillate.test.ts`): retellings score 0.44–0.55, unrelated material 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 and would have let it through. - **Units are compared within their kind.** An argument re-used as a hook is a new move, not a retread. - **A re-locked edition replaces its entry**, never duplicates it — `/linkedin:pivot` re-opens and re-locks the same edition, and the reader still only met it once. - **A missing distillate is not an error.** A series' first edition compares against nothing and is CLEAR. ## 2. Editions register (N12, A1-11 / A1-12) Two things were structurally invisible before this: **which editions are in flight** (readable only by opening every series folder's `edition-state.json` by hand) and **how long an edition takes** (not recorded anywhere at all). At a two-editions-a-week cadence both are load-bearing — you cannot dimension a production line on numbers you never collected. So every phase transition in `/linkedin:newsletter` runs **one** command that does two writes: it appends `{phase, completedAt}` to `articles.NN.phaseLog` in the edition-state, and mirrors the edition's row into the register. ```bash # at EVERY phase transition (after currentPhase has been written) node --import tsx src/cli.ts register-upsert \ --edition-state "/linkedin/edition-state.json" \ --next "Step 3a — spine prose" [--slot "2026-07-28 08:30"] [--path ] # the week's work-in-progress, without opening a single series folder node --import tsx src/cli.ts register-list [--all] [--json] # at Step 10, once the edition is scheduled — prints the measured lead time node --import tsx src/cli.ts register-complete --series seres --edition 05 ``` ### Where the register lives `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/editions/register.json` — the per-user data dir (M0 convention), because it spans *all* series and must survive plugin upgrades and reinstalls. Contrast the distillate above, which is per-series and therefore lives in the series root. ### Model - **The register is a MIRROR, never a source of truth.** Every row is derived from an `edition-state.json` at a transition. Resumption reads edition-state and nothing else; delete the register and the next transition rebuilds the row. Losing it costs telemetry, not an edition — so a failed `register-upsert` is reported, never a reason to stop the pipeline. - **One call, both writes.** A phase log the command layer had to remember to append separately would be incomplete inside a week, and an incomplete log measures nothing. - **No clock in the core.** Every mutation takes `now` as an argument (`--at` / `--now` at the CLI edge), exactly like the distillate takes `lockedAt`. Deterministic tests, no hidden timezone behaviour. - **`startedAt` never moves.** It is the lead-time anchor: re-upserting a completed edition *reactivates* the same row (that is what `/linkedin:pivot` does), so the measured time reflects real elapsed production rather than restarting the clock. - **Idempotent where a re-run is legitimate, not where it is real work.** Repeating the same transition logs once; a phase that recurs *after* another one is logged again, because a pivot back through cleared gates is production time that actually happened. Completing twice keeps the first `completedAt`. - **Missing facts are errors, not defaults.** A row naming the wrong phase is worse than no row — the whole point is that the list can be trusted without opening files. Only the title is allowed to be empty; it is telemetry, not a gate. ## Test ```bash npm test # node:test, deterministic (callers supply lockedAt) npm run build # tsc — must stay clean ```