# 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

`<serie>/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 "<serie>/linkedin/series-distillate.json" \
  --series seres --extract "<abs>/extract.json"

# Step 2.5 (before prose): check the proposed skeleton against everything published
node --import tsx src/cli.ts distil-check \
  --distillate "<serie>/linkedin/series-distillate.json" \
  --skeleton "<abs>/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 "<serie>/linkedin/edition-state.json" \
  --next "Step 3a — spine prose" [--slot "2026-07-28 08:30"] [--path <series-root>]

# 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
```
