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:
Kjell Tore Guttormsen 2026-07-25 06:55:44 +02:00
commit 657539ef09
13 changed files with 1454 additions and 20 deletions

View file

@ -722,7 +722,7 @@ if [ -x "$ED_DIR/node_modules/.bin/tsx" ]; then
ED_OUT=$( set +e; (cd "$ED_DIR" && npm test) 2>&1; echo "ED_EXIT:$?" )
ED_EXIT=$(echo "$ED_OUT" | grep -oE 'ED_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
ED_TESTS=$(echo "$ED_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
EDITIONS_TESTS_FLOOR=27
EDITIONS_TESTS_FLOOR=72
if [ "$ED_EXIT" = "0" ] && [ -n "$ED_TESTS" ] && [ "$ED_TESTS" -ge "$EDITIONS_TESTS_FLOOR" ]; then
pass "editions suite green: $ED_TESTS tests pass (floor $EDITIONS_TESTS_FLOOR)"
else
@ -1928,6 +1928,135 @@ fi
echo ""
# --- Section 16s: Editions Register + phaseLog Telemetry (N12 / A1-11, A1-12) ---
echo "--- Editions Register (N12) ---"
# N12 makes two structurally invisible things visible: WHICH editions are in flight (until
# now readable only by opening every series folder's edition-state.json) and HOW LONG an
# edition actually takes (until now not recorded at all). The register is a MIRROR - only
# edition-state.json drives resumption - so the binding worth linting is that EVERY phase
# transition writes both the phaseLog entry and the register row, from ONE call:
# (A1-12 protocol) the transition protocol is defined once and is fully wired - a compound
# predicate (verb + logged field + mirrored store), so a non-vacuity
# self-test guards it (mirrors Sections 16p/16q/16r).
# (A1-12 coverage) every canonical phase calls it - a phase with no call is a silent hole
# in the lead-time log, which is exactly how telemetry dies.
# (A1-11 store) the register module + its data-dir placement exist.
# (A1-12 schema) edition-state carries the phaseLog slot the CLI appends to.
NL_N12="commands/newsletter.md"
REG_SRC="scripts/editions/src/register.ts"
EST_SRC="scripts/editions/src/editionState.ts"
EST_TPL="config/edition-state.template.json"
BT='`'
transition_protocol() { # $1 = text; wired iff it CALLS the verb, names the field it logs AND the store it mirrors to
echo "$1" | grep -qF "register-upsert" \
&& echo "$1" | grep -qF "phaseLog" \
&& echo "$1" | grep -qF "editions/register.json"
}
TP_SELFTEST_OK=1
if ! transition_protocol "run register-upsert --edition-state <path>; it appends articles.NN.phaseLog and mirrors the row into <data>/editions/register.json"; then
TP_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired transition-protocol probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if transition_protocol "$probe"; then
TP_SELFTEST_OK=0; echo " false-positive FAIL: under-wired transition probe accepted -> $probe"
fi
done <<'NEGATIVE16S'
run register-upsert at every transition and let phaseLog grow, store left unspecified
append articles.NN.phaseLog and mirror into editions/register.json, but nothing calls the verb
run register-upsert and write editions/register.json without logging the phase
NEGATIVE16S
if [ "$TP_SELFTEST_OK" -eq 1 ]; then
pass "transition-protocol self-test: predicate needs register-upsert + phaseLog + editions/register.json (1 accepted, 3 under-wired rejected)"
else
fail "transition-protocol self-test failed - the N12 wiring lint is vacuous or over-eager"
fi
# (A1-12 protocol) the protocol is defined once, in the resumption area, fully wired
PROTO_BLOCK=$(awk '/^### Phase-transition protocol/{f=1} /^## Step 1:/{f=0} f' "$NL_N12")
if transition_protocol "$PROTO_BLOCK"; then
pass "newsletter defines a wired phase-transition protocol (register-upsert appends phaseLog + mirrors the register) (A1-11/A1-12)"
else
fail "newsletter has no wired phase-transition protocol - phase telemetry falls back to the model remembering (A1-12)"
fi
# (A1-11 invariant) the mirror rule is stated where the protocol is defined - a register read
# as source-of-truth would break deterministic resumption, the one thing edition-state owns
if echo "$PROTO_BLOCK" | grep -qF "MIRROR, never a source of truth"; then
pass "newsletter states the mirror invariant - resumption reads edition-state only (A1-11)"
else
fail "newsletter does not state that the register is a mirror - it can drift into a second source of truth (A1-11)"
fi
# (A1-12 coverage) every canonical phase transition calls the protocol
NL_FLAT=$(tr '\n' ' ' < "$NL_N12" | tr -s ' ')
MISSING_PHASES=""
for phase in lived-specifics research skeleton-pitch spine-prose draft consistency-quality \
contract-gate factcheck-sweep editorial-review persona-sweep-prelock \
headless-review annotation visual-assets lock-delivery hook-conversion-gate scheduling; do
if ! printf '%s' "$NL_FLAT" | grep -qF "run the protocol (Step 0) for ${BT}${phase}${BT}"; then
MISSING_PHASES="$MISSING_PHASES $phase"
fi
done
if [ -z "$MISSING_PHASES" ]; then
pass "all 16 canonical phases carry a transition call - the lead-time log has no holes (A1-12)"
else
fail "newsletter phases with no transition call:$MISSING_PHASES - lead time silently undercounts them (A1-12)"
fi
# (A1-12 close) Step 10 closes the row - without it every edition stays 'in flight' forever
STEP10_BLOCK=$(awk '/^## Step 10:/{f=1} /^## Distribution channel/{f=0} f' "$NL_N12")
if echo "$STEP10_BLOCK" | grep -qF "register-complete"; then
pass "newsletter Step 10 closes the register row (register-complete) - lead time becomes a measured number (A1-12)"
else
fail "newsletter Step 10 never calls register-complete - editions never leave the in-flight view (A1-12)"
fi
# (A1-11 store) the register module exists with its mutations
if grep -qF "export function upsertEdition" "$REG_SRC"; then
pass "scripts/editions exports upsertEdition - the deterministic register write exists (A1-11)"
else
fail "$REG_SRC does not export upsertEdition (A1-11)"
fi
if grep -qF "export function completeEdition" "$REG_SRC" && grep -qF "export function listEditions" "$REG_SRC"; then
pass "scripts/editions exports completeEdition + listEditions - the WIP view and its close verb exist (A1-11)"
else
fail "$REG_SRC does not export both completeEdition and listEditions (A1-11)"
fi
# (A1-11 placement) the register lives in the per-user data dir (M0 convention), not the plugin
if grep -qF "LINKEDIN_STUDIO_DATA" "$REG_SRC" && grep -qF '"editions"' "$REG_SRC" && grep -qF '"register.json"' "$REG_SRC"; then
pass "editions register resolves under the per-user data dir - it survives plugin reinstalls (M0/A1-11)"
else
fail "$REG_SRC does not resolve the register under the data dir - WIP state placed wrong (M0/A1-11)"
fi
# (A1-12 telemetry) the phase log is appended by code, not by hand
if grep -qF "export function appendPhase" "$EST_SRC"; then
pass "scripts/editions exports appendPhase - phaseLog entries are written deterministically (A1-12)"
else
fail "$EST_SRC does not export appendPhase (A1-12)"
fi
# (A1-12 schema) edition-state documents AND carries the phaseLog slot
if grep -qF '"phaseLog": "Per-article phase-transition log' "$EST_TPL"; then
pass "edition-state template documents phaseLog (A1-12 schema)"
else
fail "$EST_TPL does not document phaseLog - the telemetry field is undefined (A1-12)"
fi
if grep -qF '"phaseLog": []' "$EST_TPL"; then
pass "edition-state template carries the per-article phaseLog slot (A1-12 schema)"
else
fail "$EST_TPL has no per-article phaseLog slot - new editions start unmeasurable (A1-12)"
fi
echo ""
# --- Section 18: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count
@ -1970,12 +2099,16 @@ echo ""
# UNCONDITIONAL Section-16r checks (destillat-gate self-test + Step-2.5 destillat_gated compound grep +
# Step-8 distil-append grep + Step-8 record-usage grep + types.ts usedIn grep + bank.ts recordUsage grep +
# binding.ts boundSpecificIds grep + distillate.ts checkSkeleton grep + distillate.ts series-root
# placement grep) = 155.
# placement grep) = 155; +11 for N12's eleven UNCONDITIONAL Section-16s checks (transition-protocol
# self-test + protocol-block compound grep + mirror-invariant grep + 16-phase coverage sweep +
# Step-10 register-complete grep + register.ts upsertEdition grep + register.ts completeEdition/
# listEditions grep + register.ts data-dir placement grep + editionState.ts appendPhase grep +
# template phaseLog-doc grep + template phaseLog-slot grep) = 166.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop
# the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the
# deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh
# clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=155
ASSERT_BASELINE_FLOOR=166
TOTAL_CHECKS=$((PASS + FAIL))
if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then
pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"