feat(linkedin-studio): N12 — editions-register CLI + phaseLog-telemetri (TDD) [skip-docs]
Two things were structurally invisible: 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 two editions a week both are
load-bearing — a production line cannot be dimensioned on numbers never collected.
Register (scripts/editions, new module beside distillate.ts): one row per edition
in ${LINKEDIN_STUDIO_DATA}/editions/register.json — series, edition, title, series
path, current phase, next action, slot, startedAt/completedAt. Data-dir placement
(M0) because the register spans ALL series; the distillate is per-series and stays
in the series root. Verbs: register-upsert / register-list / register-complete.
phaseLog: articles.NN.phaseLog[{phase, completedAt}] in edition-state, additive so
schemaVersion stays 1 — pre-N12 editions load unchanged and their absence reads as
"not measured", never as zero. Per-article, mirroring articles.NN.phase: lead time
is a property of an edition.
One call per transition, both writes: newsletter.md gains a phase-transition
protocol defined once beside the resumption table, and all 16 canonical phases
invoke it. register-upsert appends the phase-log entry AND mirrors the register
row. Deliberately one command, not two — telemetry the command layer must remember
to write separately is incomplete inside a week, and an incomplete log measures
nothing. Step 10 closes the row and prints the measured lead time.
Mirror discipline: resumption still reads edition-state.json and only that. Delete
the register and the next transition rebuilds it; a failed upsert is reported, never
a reason to stop the pipeline. startedAt is the one unrecoverable value, so it never
moves — re-upserting a completed edition reactivates the same row (what
/linkedin:pivot does), keeping the clock on real elapsed production time.
Deterministic: no clock in the core (now passed in, --at/--now at the edge, as the
distillate takes lockedAt). Idempotent where a re-run is legitimate (repeated
transition logs once; completing twice keeps the first completedAt), not where it is
real work (a phase recurring after another one is logged again — a pivot back through
cleared gates is production time that happened). Missing facts are refused at the
edge, not defaulted: a row naming the wrong phase is worse than no row.
TDD (Iron Law): all three test files written first and verified red before any
implementation existed (register.test.ts + editionState.test.ts failed on missing
modules, cli-register.test.ts on "unknown command: register-upsert").
Suites: editions 27 -> 72 (floor raised) · test-runner 173 -> 184 (Section 16s: 11
unconditional greps incl. a 16-phase coverage sweep + non-vacuity self-test;
anti-erosion floor 155 -> 166) · trends 300/0 · brain 134/0 · specifics-bank 45/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
b54e450c3e
commit
657539ef09
13 changed files with 1454 additions and 20 deletions
|
|
@ -112,7 +112,7 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
|
|||
| 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"; 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` |
|
||||
| 10 | **Scheduling** | register the edition in the plugin queue/state for native scheduling; close the editions-register row (measured lead time) | `hooks/scripts/queue-manager.mjs` + `scripts/editions` (`register-complete`) |
|
||||
|
||||
> **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
|
||||
|
|
@ -280,6 +280,58 @@ Persona library: <N> personas loaded (active set chosen in Step 1)
|
|||
If the chronicle voice profile or persona library is missing, say so plainly and
|
||||
continue on the Step-0 fallback — do not fabricate either.
|
||||
|
||||
### Phase-transition protocol — one deterministic call per transition
|
||||
|
||||
Every Step below ends by writing `currentPhase` to `edition-state.json`. That
|
||||
write is a **phase transition**, and every transition runs exactly one
|
||||
deterministic command — no per-step variation, no hand-written telemetry:
|
||||
|
||||
```bash
|
||||
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-upsert \
|
||||
--edition-state "<serie>/linkedin/edition-state.json" \
|
||||
--next "<the one next concrete step>"
|
||||
```
|
||||
|
||||
Run it **after** `currentPhase` has been written, never before — the command
|
||||
reads the phase out of the state file. It does two things in one call:
|
||||
|
||||
1. **appends** `{phase, completedAt}` to `articles.NN.phaseLog` — the lead-time
|
||||
telemetry (A1-12); and
|
||||
2. **mirrors** the edition into the editions register at
|
||||
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/editions/register.json`
|
||||
— series, edition, title, series path, phase, next action, slot (A1-11).
|
||||
|
||||
One call, both writes: a log the command layer had to remember to append
|
||||
separately would be incomplete within a week, and an incomplete log measures
|
||||
nothing. Add `--slot "<YYYY-MM-DD HH:MM>"` once a publishing slot is claimed, and
|
||||
`--path <series-root>` when the state file is not at the conventional
|
||||
`<serie>/linkedin/edition-state.json` location.
|
||||
|
||||
The week's work-in-progress is then one command away, with no series folder
|
||||
opened:
|
||||
|
||||
```bash
|
||||
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-list
|
||||
```
|
||||
|
||||
**At Step 10 only**, once the edition is scheduled, close its row (this is what
|
||||
turns `startedAt` into a measured lead time):
|
||||
|
||||
```bash
|
||||
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-complete \
|
||||
--series "<slug>" --edition "<NN>"
|
||||
```
|
||||
|
||||
> **The register is a MIRROR, never a source of truth.** Resumption reads
|
||||
> `edition-state.json` and nothing else (the table above). The register is a
|
||||
> read-only view for humans; if it is deleted or drifts, the next transition
|
||||
> repairs it and nothing about the edition is lost. So a failing `register-upsert`
|
||||
> is a telemetry problem to report plainly — never a reason to stop the pipeline
|
||||
> or to re-do the phase.
|
||||
|
||||
Each Step below marks its transition **↳ Phase transition** — that marker means
|
||||
"run the protocol above for this phase".
|
||||
|
||||
## Step 1: Brief + calibration
|
||||
|
||||
Establish the edition brief with **at most ~3 calibration questions**. Infer
|
||||
|
|
@ -487,6 +539,8 @@ Edition brief
|
|||
doc-string's "Resolved at Step 1" claim true.
|
||||
- Write `articles.NN.livedSpecifics` (the slot-map above, `status: "pending"`).
|
||||
- Set `currentPhase: "lived-specifics"` in `edition-state.json`.
|
||||
- **↳ Phase transition** — run the protocol (Step 0) for `lived-specifics`,
|
||||
`--next "Step 2 — research (scoped by the binding)"`.
|
||||
- Write a "lived-specifics bound → next: research (scoped by the binding)" line
|
||||
to `<serie>/STATE.md` (overwrite).
|
||||
|
||||
|
|
@ -593,8 +647,10 @@ Next: Step 2 — Research (scoped by the binding: fill ekstern/unresolved, verif
|
|||
"research complete → next: skeleton + section pitch (BEFORE prose)" next-step
|
||||
line to `<serie>/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).
|
||||
first. **↳ Phase transition** — run the protocol (Step 0) for `research`,
|
||||
`--next "Step 2.5 — skeleton + section pitch"`. 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.
|
||||
|
|
@ -806,6 +862,8 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
|
|||
resonance/conversion under the same `personaSweep` object).
|
||||
- `currentPhase: "skeleton-pitch"` in `edition-state.json` (the marker that
|
||||
Step 2.5 is complete and the gate has passed).
|
||||
- **↳ Phase transition** — run the protocol (Step 0) for `skeleton-pitch`,
|
||||
`--next "Step 3a — spine prose"`.
|
||||
- A "skeleton + pitches PASS (primær JA) → next: Step 3a (spine prose)"
|
||||
next-step line in `<serie>/STATE.md` (overwrite).
|
||||
|
||||
|
|
@ -914,6 +972,8 @@ Typically ~20–30 % of the edition's final length.
|
|||
with the expanded prose).
|
||||
- `currentPhase: "spine-prose"` in `edition-state.json` (the marker that 3a
|
||||
is complete and the gate has passed).
|
||||
- **↳ Phase transition** — run the protocol (Step 0) for `spine-prose`,
|
||||
`--next "Step 3b — full prose expansion"`.
|
||||
- A "spine prose JA → next: Step 3b (full prose expansion)" next-step line in
|
||||
`<serie>/STATE.md` (overwrite).
|
||||
|
||||
|
|
@ -988,6 +1048,9 @@ turning-points the spine already named.
|
|||
silently skip any draft without an `NN` prefix). Set
|
||||
`currentPhase: "draft"` in `edition-state.json`, and write a "draft
|
||||
complete → next: consistency/quality" line to `<serie>/STATE.md` (overwrite).
|
||||
**↳ Phase transition** — run the protocol (Step 0) for `draft`,
|
||||
`--next "Step 4 — consistency + quality"`. Run it only when the draft is
|
||||
COMPLETE, not at an interim cursor save: the log measures finished phases.
|
||||
|
||||
```
|
||||
Full prose expansion — complete (or: partial — resumes at section <X>).
|
||||
|
|
@ -1052,7 +1115,8 @@ non-self-certified — that verdict stays with the Step 6 persona sweep / operat
|
|||
|
||||
After the pass, set `currentPhase: "consistency-quality"` in `edition-state.json`
|
||||
and write a "quality pass complete → next: contract-gate" line to
|
||||
`<serie>/STATE.md` (overwrite).
|
||||
`<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the protocol
|
||||
(Step 0) for `consistency-quality`, `--next "Step 4.5 — contract-gate"`.
|
||||
|
||||
```
|
||||
Consistency + quality pass complete.
|
||||
|
|
@ -1123,7 +1187,8 @@ mechanical noise.
|
|||
|
||||
3. **Persist + checkpoint state.** Set `currentPhase: "contract-gate"` in
|
||||
`edition-state.json` and write a "contract-gate passed → next: fact-check sweep"
|
||||
line to `<serie>/STATE.md` (overwrite).
|
||||
line to `<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the
|
||||
protocol (Step 0) for `contract-gate`, `--next "Step 5 — fact-check sweep"`.
|
||||
|
||||
```
|
||||
Contract-gate complete.
|
||||
|
|
@ -1198,6 +1263,8 @@ because it "feels" right or because it sits in your own research notes.
|
|||
record of what was checked — this is where the old HANDOVER §4 log now lives),
|
||||
set `currentPhase: "factcheck-sweep"`, and write a "fact-check complete → next:
|
||||
persona sweep (BEFORE lock)" line to `<serie>/STATE.md` (overwrite).
|
||||
**↳ Phase transition** — run the protocol (Step 0) for `factcheck-sweep`,
|
||||
`--next "Step 5.5 — editorial review"`.
|
||||
|
||||
```
|
||||
Fact-check sweep complete.
|
||||
|
|
@ -1319,6 +1386,8 @@ and `persona-reviewer` never flags em-dash density (that is this step).
|
|||
machine-readable record — what was flagged, severity, fold-in decision).
|
||||
- Set `currentPhase: "editorial-review"` in `edition-state.json` (the marker
|
||||
that Step 5.5 is complete and the operator-gate has passed).
|
||||
- **↳ Phase transition** — run the protocol (Step 0) for `editorial-review`,
|
||||
`--next "Step 6 — persona sweep (pre-lock)"`.
|
||||
- Write an "editorial review complete (craft clean) → next: persona sweep
|
||||
(BEFORE lock)" line to `<serie>/STATE.md` (overwrite).
|
||||
|
||||
|
|
@ -1393,7 +1462,9 @@ reopening locked texts — the biggest single process error of the series (plan
|
|||
heuristic (Step 8 / `/linkedin:pivot`) compares against to catch a late pivot.
|
||||
Set `currentPhase: "persona-sweep-prelock"`, and write a "persona sweep
|
||||
PASS (primær JA) → next: headless adversarial review (Step 6.5, BEFORE lock)"
|
||||
line to `<serie>/STATE.md` (overwrite).
|
||||
line to `<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the
|
||||
protocol (Step 0) for `persona-sweep-prelock`,
|
||||
`--next "Step 6.5 — headless adversarial review"`.
|
||||
|
||||
```
|
||||
Persona sweep complete (BEFORE lock).
|
||||
|
|
@ -1507,7 +1578,8 @@ command runs — see `commands/headless-review.md` for the full cold contract):
|
|||
summary, status}`, `consolidatedReport`, `foldedIn`/`waived`, `status:
|
||||
"folded"`), set `currentPhase: "headless-review"`, and write a "headless review
|
||||
complete (cold, converged flags folded) → next: annotation/lock" line to
|
||||
`<serie>/STATE.md` (overwrite).
|
||||
`<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the protocol
|
||||
(Step 0) for `headless-review`, `--next "Step 7/7.5 — annotation + visual assets"`.
|
||||
|
||||
```
|
||||
Headless adversarial review (cold, BEFORE lock) — complete.
|
||||
|
|
@ -1565,6 +1637,9 @@ editor is satisfied with the in-session draft. It does not gate lock.
|
|||
4. **Persist.** Set `currentPhase: "annotation"` in `edition-state.json` and
|
||||
write an "annotation rendered (optional) → next: visual assets" line to
|
||||
`<serie>/STATE.md` (overwrite). If skipped, note "annotation skipped" and move on.
|
||||
**↳ Phase transition** — run the protocol (Step 0) for `annotation`,
|
||||
`--next "Step 7.5 — visual assets"`. If the step was skipped, skip the
|
||||
transition too: an unrun phase must not appear in the lead-time log.
|
||||
|
||||
```
|
||||
Annotation (optional).
|
||||
|
|
@ -1722,6 +1797,8 @@ the deck is approved) and credit/caption are recorded:
|
|||
`edition-state.json`.
|
||||
- Set `currentPhase: "visual-assets"` in `edition-state.json` (the marker that
|
||||
Step 7.5 is complete and the gate has passed).
|
||||
- **↳ Phase transition** — run the protocol (Step 0) for `visual-assets`,
|
||||
`--next "Step 8 — LOCK + delivery"`.
|
||||
- Write a "visual assets approved (cover/figures or carousel) → next:
|
||||
lock/delivery" line to `<serie>/STATE.md` (overwrite).
|
||||
|
||||
|
|
@ -1815,6 +1892,8 @@ produces the editor's single delivery artifact — `POST.html`, the
|
|||
`<serie-mappe>/linkedin/NN/POST.html` as a `file://` link. From here the
|
||||
body is frozen — Step 9 may only adjust the distribution hook, never the
|
||||
locked edition text.
|
||||
**↳ Phase transition** — run the protocol (Step 0) for `lock-delivery`,
|
||||
`--next "Step 9 — hook/conversion gate"`.
|
||||
|
||||
5. **Auto-gull — grow the gold corpus (A2-F2).** A locked edition is now
|
||||
published-grade voice signal, so append it to the gold corpus the chronicle
|
||||
|
|
@ -1974,6 +2053,8 @@ lock invariant without breaking phase order.
|
|||
`articles.NN.randsoneGate` (per-surface fact/language verdicts + title-strength +
|
||||
what was revised) and the conversion verdict in
|
||||
`articles.NN.personaSweep.conversion`; set `currentPhase: "hook-conversion-gate"`.
|
||||
**↳ Phase transition** — run the protocol (Step 0) for `hook-conversion-gate`,
|
||||
`--next "Step 10 — scheduling"`.
|
||||
|
||||
```
|
||||
Randsone + conversion gate (post-lock, distribution copy only).
|
||||
|
|
@ -2038,6 +2119,20 @@ now a first-class scheduled post.
|
|||
`edition-state.json`. Write a closing "edition scheduled → mark live via
|
||||
`/linkedin:calendar` on <date>" line to `<serie>/STATE.md` (overwrite). The
|
||||
pipeline is complete.
|
||||
**↳ Phase transition** — run the protocol (Step 0) for `scheduling`,
|
||||
`--next "published — mark live via /linkedin:calendar"`, and then **close the
|
||||
register row** (this step alone does):
|
||||
|
||||
```bash
|
||||
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-complete \
|
||||
--series "<slug>" --edition "<NN>"
|
||||
```
|
||||
|
||||
It prints the measured lead time (first transition → completion) — the A1-12
|
||||
number the cadence work is calibrated on. An edition re-opened later by
|
||||
`/linkedin:pivot` simply re-activates the same row on its next transition; the
|
||||
original `startedAt` is kept, so the clock reflects the real elapsed production
|
||||
time rather than restarting.
|
||||
|
||||
```
|
||||
Scheduling.
|
||||
|
|
@ -2118,7 +2213,7 @@ the honest decision surface; it sells nothing.
|
|||
|
||||
- `${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), `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}/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. **Editions register (N12):** `register-upsert` at every phase transition (appends `articles.NN.phaseLog` + mirrors the row into `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/editions/register.json`), `register-list` for the work-in-progress view, `register-complete` at Step 10 (measured lead time). The register mirrors state — resumption never reads it
|
||||
- `${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