From 4fad655a664aad6a7a0083e621f36a92cdc98332 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 18 Jun 2026 18:34:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(linkedin-studio):=20S11=20harden=20pivot=20?= =?UTF-8?q?=E2=80=94=20moderate=20to-phase=20reset=20skipped=20fact-check?= =?UTF-8?q?=20(off-by-one)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE --- commands/pivot.md | 20 ++++++++--- docs/hardening/log.md | 82 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/commands/pivot.md b/commands/pivot.md index be3ab6a..7ae0a35 100644 --- a/commands/pivot.md +++ b/commands/pivot.md @@ -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 "" (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[]`: diff --git a/docs/hardening/log.md b/docs/hardening/log.md index bad6210..c268d15 100644 --- a/docs/hardening/log.md +++ b/docs/hardening/log.md @@ -939,3 +939,85 @@ cannot be (`:30–48`). - 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 1–2: **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 `/STATE.md`, +> and deferring the actual gate-runs to `/linkedin:newsletter` (`:144`). Feed-post predicates +> (hook 110–140, 1,200–1,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, `:33–38`): 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 `:84–86`; ambiguous sections → confirm `:90`) → **Step 2 classify → default `--to-phase`** + → Step 3 append `pivots[]` + reset `currentPhase` + un-lock + invalidate verdicts (`:100–131`) → + Step 4 write next-step line to `/STATE.md`, defer gates to newsletter (`:133–155`). +- **Friction sought (the S2–S8 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 (`:1348–1362`) == template `_doc.pivots` (`:30`). Newsletter + Step 6 actually writes the baseline field (`:990–994`, `:1005`) → not a phantom. Cardinal rule + ("never reopen a locked text", newsletter `:1034–1036`) honoured: pivot is the *sanctioned* exception + — it loudly un-locks + flags POST.html stale (`:124`) + invalidates verdicts (`:126–130`). +- **(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 `:84–86` · ambiguous-sections `:90`). 3/3 ref + files exist. + +**THE BUG (P4 — confirmed cross-file off-by-one).** Newsletter's resumption table is explicit +(`:196–198`, `:203–221`): `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" (`:116–119`). 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 `:96–97`: `factcheck-sweep` → **`consistency-quality`** (resumes Step 5, fact-check + runs) — the bug. +2. Structural label `:92–95`: "(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 `:102–108`: 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 `:64–70` (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. + +---