fix(linkedin-studio): S11 harden pivot — moderate to-phase reset skipped fact-check (off-by-one)

S11 hardening of /linkedin:pivot. Class: pure state-mutation/orchestration
command, NOT post-emitting, invokes no agents — re-opens a long-form edition so
cleared gates re-run before lock. Feed-post predicates N/A.

BUG (P4, cross-file off-by-one): newsletter's resumption table treats
currentPhase as the LAST COMPLETED phase and resumes at the NEXT step
(draft->Step 4, consistency-quality->Step 5, factcheck-sweep->Step 5.5). The
moderate pivot default set to-phase: factcheck-sweep "so the new claims get
verified" — which resumes at Step 5.5 and SKIPS the Step 5 fact-check the
moderate pivot exists to run, violating the command's own invariant that a pivot
always invalidates the fact-check.

FIX (4 surgical edits, commands/pivot.md only):
1. moderate default factcheck-sweep -> consistency-quality (resumes Step 5)
2. structural label "(Step 3b)" -> "Step 4 (consistency)"
3. off-by-one guard callout explaining to-phase = currentPhase (last-completed)
4. anatomy enum drops factcheck-sweep -> draft | consistency-quality
gatesToRerun list + worked example unchanged (both correct).

4-axis verdict: a/b/c/d all PASS post-fix, 0 deferrals -> FIXED.
Gate: test-runner 81/0/0 · counts 29/19/26 unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 18:34:37 +02:00
commit 4fad655a66
2 changed files with 97 additions and 5 deletions

View file

@ -43,7 +43,7 @@ edition can sail into lock carrying an unverified premise or an unread argument.
/linkedin:pivot
--article NN (required; the edition article that changed)
--reason "<one line>" (required; e.g. "Security Champions-anker")
--to-phase draft | consistency-quality | factcheck-sweep (optional; default from the heuristic)
--to-phase draft | consistency-quality (optional; default from scope — see Step 2)
```
## The pivot-detection heuristic
@ -90,13 +90,23 @@ cleared, the lock STOPS and points the operator here. (Length-band drift itself
the operator if ambiguous).
4. **Classify scope** (drives the default `--to-phase`):
- **Structural** (deltaPct > 20 % OR newSections > 2, or a new axis/thesis) →
default `to-phase: draft` (Step 3b). The new material needs full prose
expansion → consistency → fact-check → editorial → persona → headless.
default `to-phase: draft` so newsletter resumes at **Step 4
(consistency-quality)** — the changed prose is re-threaded, then fact-check
(5) → editorial (5.5) → persona (6) → headless (6.5) re-run on it.
- **Moderate** (new examples/claims, no new sections, deltaPct ≤ 20 %) →
default `to-phase: factcheck-sweep` (Step 5) so the new claims get verified,
then editorial + persona + headless re-run.
default `to-phase: consistency-quality` so newsletter resumes at **Step 5
(fact-check)** and the new claims get verified, then editorial (5.5) →
persona (6) → headless (6.5) re-run.
- The operator may override with explicit `--to-phase`.
> **Why these specific phases (the off-by-one trap).** `to-phase` is written as
> `currentPhase`, which records the *last completed* phase; `/linkedin:newsletter`'s
> resumption table (Step 0) then resumes at the step **after** it. So `draft`
> resumes at **Step 4** and `consistency-quality` resumes at **Step 5** (the
> fact-check). A pivot therefore never resets to `factcheck-sweep` — that would
> resume at Step 5.5 and **skip** the fact-check, violating this command's
> invariant that a pivot always invalidates the fact-check (Step 3, `gatesToRerun`).
## Step 3 — Log the pivot + reset the phase (the ritual)
1. **Append a pivot entry** to `articles.NN.pivots[]`:

View file

@ -939,3 +939,85 @@ cannot be (`:3048`).
- Disposition: **VERIFIED — NO CHANGE** (0 edits) · 0 deferrals · axes a/b/c/d all PASS.
---
## Session 11 — Longform companion (`pivot`)
> S11 status: re-grounded STATE's predicates against the actual `pivot.md` (162 lines) BEFORE talk.
> Class agreed steps 12: **pure state-mutation / orchestration command, NOT post-emitting, invokes NO
> agents, never drafts prose.** It re-opens a long-form edition after a late substantive change so the
> cleared gates re-run before lock — mutating `edition-state.json` (append `pivots[]`, reset
> `currentPhase`, un-lock, invalidate downstream verdicts), writing a next-step line to `<serie>/STATE.md`,
> and deferring the actual gate-runs to `/linkedin:newsletter` (`:144`). Feed-post predicates
> (hook 110140, 1,2001,800 band, no-body-link) do NOT apply — even further from feed-post than
> `headless-review` (this one does not even produce a review of prose). The pivot heuristic + `pivots[]`
> schema + gates-to-rerun chain are SHARED with `newsletter` (Step 8) and `headless-review`
> S9/S10 findings reused as baseline. **Unlike S9/S10 this was NOT a no-change session — P4 surfaced a
> real cross-file off-by-one bug.**
### /linkedin:pivot — re-open a cleared edition for re-review, NOT post-emitting
**INTENT.** Make the post-lock re-open a NAMED RITUAL, not a manual scramble (the Del 4 Security
Champions case, `:3338`): log the pivot, reset `currentPhase` so newsletter's deterministic resumption
re-runs the invalidated gates (fact-check 5 → editorial 5.5 → persona 6 → headless 6.5), un-lock if
needed, and invalidate the stale downstream verdicts so a pre-pivot PASS cannot be mistaken for current.
**SIMULATE (grounded persona-sim).**
- **Persona (operator):** KTG having made a late change to a LOCKED edition, running
`/linkedin:pivot --article NN --reason "<...>"`.
- **Walk:** Step 1 load state + locate article (missing → stop+report `:75`) → Step 2 measure scope
(`wc -w` current vs baseline `personaSweep.resonance.wordCount`; baseline-absent → ask/default
structural `:8486`; ambiguous sections → confirm `:90`) → **Step 2 classify → default `--to-phase`**
→ Step 3 append `pivots[]` + reset `currentPhase` + un-lock + invalidate verdicts (`:100131`) →
Step 4 write next-step line to `<serie>/STATE.md`, defer gates to newsletter (`:133155`).
- **Friction sought (the S2S8 pattern):** **FOUND — a phase-reset off-by-one (P4).**
**EVALUATE (4 axes — re-tolket for a non-post-emitting orchestrator).**
- **(a) intention fidelity — PASS after fix.** The ritual (log → reset → un-lock → invalidate → point)
delivers the promise; `allowed-tools` complete and used. The off-by-one (below) made the *moderate*
branch silently betray the stated intent ("so the new claims get verified") → fixed.
- **(b) format bar (heuristic-consistency, NOT feed predicates) — PASS.** Three-way SSOT verified
identical: pivot heuristic (>20 % words OR >2 sections, baseline = `personaSweep.resonance.wordCount`)
== newsletter Step 8 lock-precondition (`:13481362`) == template `_doc.pivots` (`:30`). Newsletter
Step 6 actually writes the baseline field (`:990994`, `:1005`) → not a phantom. Cardinal rule
("never reopen a locked text", newsletter `:10341036`) honoured: pivot is the *sanctioned* exception
— it loudly un-locks + flags POST.html stale (`:124`) + invalidates verdicts (`:126130`).
- **(c) quality rules (state-integrity) — PASS.** `gatesToRerun` `:113` = the 4 real gate phase-names;
invalidation targets (`personaSweep.resonance` · `editorialReview` · `headlessReview.status`) all
exist in the schema; `factcheckLog` + `pivots[]` left intact (durable history `:130`).
- **(d) agent-wiring + graceful degradation — PASS.** No agents (orchestrator). 3 degradation points
enumerated (article-missing `:75` · baseline-absent `:8486` · ambiguous-sections `:90`). 3/3 ref
files exist.
**THE BUG (P4 — confirmed cross-file off-by-one).** Newsletter's resumption table is explicit
(`:196198`, `:203221`): `currentPhase` = the *last completed* phase, and the pipeline resumes at the
step **after** it (`draft`→Step 4, `consistency-quality`→Step 5, `factcheck-sweep`→Step 5.5). But
pivot.md set the **moderate** default to `to-phase: factcheck-sweep` "so the new claims get verified" —
which resumes at **Step 5.5 (editorial), SKIPPING the Step 5 fact-check** the moderate pivot exists to
run. Self-contradictory, and a violation of the command's own invariant that "a pivot always invalidates
the fact-check" (`:116119`). The **structural** branch (`to-phase: draft`→Step 4→fact-check at 5) was
functionally safe but mis-labelled "(Step 3b)". Root cause: conflating "the phase to resume AT" with the
`currentPhase` (last-completed) value to write.
**THE FIX (4 surgical edits, confined to command-anatomy + Step 2).**
1. Moderate default `:9697`: `factcheck-sweep`**`consistency-quality`** (resumes Step 5, fact-check
runs) — the bug.
2. Structural label `:9295`: "(Step 3b) … full prose expansion" → "resumes **Step 4
(consistency-quality)** … then fact-check (5) → editorial (5.5) → persona (6) → headless (6.5)".
3. Off-by-one guard callout `:102108`: explains `to-phase` is a `currentPhase` (last-completed) value
and newsletter resumes at the NEXT step → why a pivot never resets to `factcheck-sweep`.
4. Anatomy enum `:46`: dropped `factcheck-sweep` from the offered values (it skips fact-check, violating
the always-invalidate-fact-check invariant) → `draft | consistency-quality`.
`gatesToRerun` `:113` and the worked example `:6470` (structural, `draft`) left intact — both correct.
**VERIFY.**
- Re-grepped final file: `factcheck-sweep` now only at `:106` (guard, as the forbidden value) + `:123`
(gatesToRerun gate-name); to-phase defaults = `draft` (`:93`) / `consistency-quality` (`:97`); enum
`:46` = two values; guard landed `:102`.
- `bash scripts/test-runner.sh``Passed: 81 · Failed: 0 · Warnings: 0`; counts **29/19/26** unchanged
(count/version/model-consistency guards passed; .md-only edit, node/analytics tests not triggered).
- **One NICE left (anti-gold-plating):** template `:30` says "+~530 words" (sum of the 2 named sections)
vs worked-example 1400→1992 = +592; both "~", both +42 % — cosmetic, below the axis-pass bar, left.
- Disposition: **FIXED** (4 edits, all in `commands/pivot.md`) · 0 deferrals · axes a/b/c/d all PASS
post-fix.
---