Skip to content

fix(spec-review): an unvouchable pin marker narrows the window to n (FAFF-1324) - #1087

Merged
alechill merged 4 commits into
mainfrom
FAFF-1324-pin-marker-contradiction-disposition
Oct 17, 2026
Merged

alechill merged 4 commits into
mainfrom
FAFF-1324-pin-marker-contradiction-disposition

Conversation

@alechill

Copy link
Copy Markdown
Contributor

Warning: Ignoring extra certs from /Users/shftwst/.claude/.cage-relay/ca-certs.pem, load failed: error:80000002:system library::No such file or directory
Closes FAFF‑1324.

A spike, so the deliverable is the decision plus the localised change it implies. The change is three lines of govern(); the decision is the substance.

The decision

When pin-capture.json cannot vouch for which round established the pin, govern() now takes the conservative end of the unknown range. The else arm of the marker ternary sets capRound = n instead of 1, and the late-pin rule reports unpinnable-reset when it fires with an anomaly attached (late-pin-advance when the anomaly is null). Exit codes unchanged, no field added to GovernanceResult, no anomaly or action token minted.

Two grounds, in order of weight.

  1. The fail-safe idiom for this exact predicament is already settled twelve lines above. The pin === null arm narrows to n and its comment calls that a fail-safe the governor MUST take. Four of the six anomaly arms narrow to n, and prep narrows at both of its adjacent seams. The marker arms were the only ones that widened by omission, and nothing documented that asymmetry as intentional.
  2. window_start is published as the span the loop can vouch for, and with capRound = 1 it was not. That is a correctness claim about the artifact rather than about outcomes, and it is the claim the spike was opened to settle.

The decision covers pin-marker-missing identically. The two are one ternary with one severity and one fallback, and readCaptureOutcome returns null for an absent and a torn marker alike, by documented design.

Be precise about the trade

This does not buy a safer outcome, and the PR should not be read as claiming it does.

A wider window is biased toward over-parking, never over-yielding: converging requires strictly_decreasing across every consecutive pair in the window and no_churn across every step, both strictly harder to satisfy as the window widens. spec-review-window.js:18-21 says so outright. Both dispositions therefore terminate at needs-human.

What this buys is a comparison the governor can vouch for. What it costs is the one case where a correct yield was still reachable, every round genuinely served by one reviewer, plus the remaining rounds of the appetite cap. The status quo did not yield false convergence.

FAFF‑1052 deferred this question rather than deciding it: the window-narrowing disposition sits in its Proposed direction, under "Not settled, for prep to shape", not in its Acceptance, and it shipped with capRound = 1 in a line it wrote. This change makes no claim to repair an acceptance breach.

The accepted latch, and its owner

Nothing repairs an unvouchable marker, so every later governed round also narrows to n and the convergence window becomes [n..n] for the rest of the loop. converging is then deterministically false, the loop-cap yield never fires, and the loop runs to the appetite ceiling and parks. That is a real interim cost, accepted deliberately, and it is reachable rather than theoretical: two live causes of an unvouchable marker remain.

Both now have owners, which is what makes the deferral honest rather than silent:

  • FAFF‑1331 — the marker self-heal, citing capturePin's existing-pin arm. Its open question is sharper than the spec assumed: pinned-reviewer.json stores no round, so the capture round may not be recoverable at all, and guessing it would restore a window the loop still cannot vouch for.
  • FAFF‑1332 — the unguarded --backends-json failure write at spec-review-pin.js:382. FAFF‑1052 added that write deliberately for its never-silent property, so dropping it is the wrong remedy; the ticket records that constraint.

Verification

  • Window suite 35/35. Sibling suites (occupant-served-dispatch, atomic-write, pin) 46/46, none edited.
  • faff spec-review-window --selftest PASS against the modified lib. Both marker twins now assert action and window_start; the contradiction twin needed a window reset because it shares a scratch dir with its sibling, whose earlier govern() call had already persisted window_start = 3. Without it the twin could never reach the late-pin rule. That reset is an addition beyond the spec's DoD text and was disclosed rather than quietly made.
  • Three new cases: window_start already at n (held, never lowered), round 1 (inert), and an unvouchable marker alongside a genuine swap (swap-reset, anomaly retained).
  • The spec's integration smoke test reproduces exactly: exit 0, window_start: 3, action: "unpinnable-reset", anomaly: "pin-marker-contradiction", governance-3.json matching stdout, window.json re-read as {"window_start":3}.
  • Pre-PR review reached pass on round 2. Round 1's major was the unfiled follow-ups above; its observation (a fixture's hardcoded winner_index) is fixed.

The local gate ladder reports fail on its UNIT rung for a pre-existing reason, not this change: a baseline run on an unmodified checkout had 46 failures against this worktree's 45, and a sorted diff of the failing-test lists showed zero unique to this change. LINT, TYPECHECK and STATIC_ANALYSIS rungs all pass.

Plain JavaScript, no TypeScript sibling, so there is no emit to rebuild.

Acceptance Criteria

  • faff-1324#done:1 A governed round with an unvouchable marker and window_start < n persists window_start: n, so the convergence window no longer spans rounds whose shared reviewer is unproven.
    Verified: Integration smoke test run: govern round 3 over a pin with a {state:failed,round:1} marker and window_start 1 persisted window_start 3, action unpinnable-reset, anomaly pin-marker-contradiction; window.json re-read as {"window_start":3}.
    Method: command faff spec-review-window --govern --dir <scratch> --round 3 --any-served
  • faff-1324#done:2 The commit message records the decision and rests it on the fail-safe idiom and on window_start's published meaning, states the trade in the direction Failure modes establishes (a wide window is biased toward over-parking, so this change buys a vouchable comparison rather than a safer outcome), and notes that FAFF‑1052 deferred this question rather than deciding it. It must not claim the status quo yielded false convergence, and must not claim it breached FAFF‑1052's acceptance.
    Verified: Commit 02a45a9's message rests the decision on the fail-safe idiom and on window_start's published meaning, states the over-parking direction and that the change buys a comparison the governor can vouch for rather than a safer outcome, and records that FAFF‑1052 deferred the question. Neither forbidden claim appears: no false-convergence claim, no acceptance-breach claim.
    Method: command git log -1 --format=%B
  • faff-1324#done:3 GovernanceResult carries exactly round, window_start, action, anomaly, served_identity, capture_state, capture_round, ts; test/spec-review-window.test.mjs:418-432 passes with its key list unmodified.
    Verified: Key-set test passes with its list unmodified; no field added to GovernanceResult.
    Method: test test/spec-review-window.test.mjs::govern: every non-arg-error path writes governance-<n>.json with the full result shape
  • faff-1324#done:4 No AnomalyToken or ActionToken value is added, renamed, or removed; grep -c '"unpinnable-reset"' plugin/skills/faff/bin/lib/spec-review-window.js shows the token reused, not replaced.
    Verified: grep -c '"unpinnable-reset"' returns 6 — the token is reused, not replaced; no token added, renamed or removed.
    Method: command grep -c '"unpinnable-reset"' plugin/skills/faff/bin/lib/spec-review-window.js
  • faff-1324#done:5 spec-review-window.js:245 — the else arm — sets capRound = n; the literal capRound = 1 no longer appears in govern(). Do not edit :244: that is the clean cap.state === "captured" arm, and setting capRound = n there would break late-pin-advance and fail test/spec-review-window.test.mjs:308.
    Verified: The else arm sets capRound = n and the literal capRound = 1 no longer appears anywhere in the file (0 occurrences). The arm now sits at :247 rather than :245 because the explanatory comment added above it shifted the line; the clean captured arm was not touched.
    Method: command grep -n 'capRound = n' plugin/skills/faff/bin/lib/spec-review-window.js; grep -c 'capRound = 1' plugin/skills/faff/bin/lib/spec-review-window.js
  • faff-1324#done:6 The late-pin rule reports unpinnable-reset when it fires with a non-null anomaly and late-pin-advance when it fires with anomaly == null.
    Verified: Rule (L) reads action = anomaly === null ? "late-pin-advance" : "unpinnable-reset" at :251; both branches are covered by the suite.
    Method: test test/spec-review-window.test.mjs::govern: late-pin-advance — the pin is first captured at round 2 (the FAFF‑996 recovery case)
  • faff-1324#done:7 test/spec-review-window.test.mjs:347 (govern: soft pin-marker-missing ... full GovernanceResult asserted) is updated: action changes from "unchanged" to "unpinnable-reset", and a new assert.equal(result.window_start, 3) is added. code: 0, anomaly: "pin-marker-missing", capture_state: "absent", capture_round: null are all asserted unchanged.
    Verified: The pin-marker-missing test now asserts action unpinnable-reset and window_start 3; its code, anomaly, capture_state and capture_round assertions are unchanged.
    Method: test test/spec-review-window.test.mjs::govern: soft pin-marker-missing (legacy pin, no capture marker) — full GovernanceResult asserted
  • faff-1324#done:8 test/spec-review-window.test.mjs:362 (the pin-marker-contradiction twin) gains assert.equal(result.action, "unpinnable-reset") and assert.equal(result.window_start, 3). Its existing code: 0, anomaly, and capture_state: "failed" assertions pass unmodified; this test asserts no action or window_start today, so it does not fail before the edit. The assertions are added so the new behaviour is pinned rather than merely tolerated.
    Verified: The contradiction twin gained the action and window_start assertions; its existing assertions are untouched.
    Method: test test/spec-review-window.test.mjs::govern: soft pin-marker-contradiction (pin present, capture marker state != captured) — full GovernanceResult asserted
  • faff-1324#done:9 The selftest twin at spec-review-window.js:556 (govern pin-marker-missing (soft, non-fatal)) asserts r.result.action === "unpinnable-reset" and r.result.window_start === 3 in place of action === "unchanged", keeping r.code === 0 and the anomaly check.
    Verified: Selftest twin asserts action unpinnable-reset and window_start 3, run via the worktree's own binary (the modified lib), not the main checkout's.
    Method: command ./plugin/skills/faff/bin/faff spec-review-window --selftest
  • faff-1324#done:10 The selftest twin at spec-review-window.js:560 (govern pin-marker-contradiction (soft, non-fatal)) is extended from code-and-anomaly only to also assert action === "unpinnable-reset" and window_start === 3, closing the asymmetry with its sibling.
    Verified: The contradiction selftest twin now asserts action and window_start too, closing the asymmetry with its sibling. It needed a writeWindowStart(tmp, 1) reset, because it shares a scratch dir with the sibling whose govern call had already persisted window_start 3; without it the twin could not reach the late-pin rule at all. This fixture reset is an addition beyond the spec's literal DoD text.
    Method: command ./plugin/skills/faff/bin/faff spec-review-window --selftest
  • faff-1324#done:11 faff spec-review-window --selftest reports PASS and exits 0, so test/spec-review-window.test.mjs:250,256 pass.
    Verified: RESULT: PASS (spec-review-window, 0 failed), exit 0, against the modified lib.
    Method: command ./plugin/skills/faff/bin/faff spec-review-window --selftest
  • faff-1324#done:12 A governed round with an unvouchable marker and window_start already equal to n reports action: "unchanged", window_start: n, anomaly recorded; covered by a new test case.
    Verified: New case: window_start already equal to the round reports unchanged with window_start held, anomaly recorded, exit 0 — never lowered.
    Method: test test/spec-review-window.test.mjs::govern: unvouchable marker, window_start already n — unchanged, never lowered
  • faff-1324#done:13 Round 1 with an unvouchable marker is unchanged from today: window_start: 1, action: "unchanged"; covered by a new test case.
    Verified: New case: round 1 is inert — window_start 1, action unchanged, exit 0.
    Method: test test/spec-review-window.test.mjs::govern: unvouchable marker at round 1 — window_start 1, unchanged
  • faff-1324#done:14 An unvouchable marker plus a served identity differing from the pin still reports action: "swap-reset" with the anomaly attached; covered by a new test case.
    Verified: New case: an unvouchable marker plus a differing served identity still reports swap-reset with the anomaly attached.
    Method: test test/spec-review-window.test.mjs::govern: unvouchable marker plus a swapped served identity — swap-reset with the anomaly attached
  • faff-1324#done:15 test/spec-review-window.test.mjs:308 (the healthy late-pin-advance path) and the unchanged and swap-reset tests pass unmodified, proving the clean branch is untouched.
    Verified: Whole window suite green at 35/35, including the healthy late-pin-advance path and the unchanged and swap-reset tests, none of which were edited.
    Method: test test/spec-review-window.test.mjs::govern: late-pin-advance — the pin is first captured at round 2 (the FAFF‑996 recovery case)
    Method: test test/spec-review-window.test.mjs::govern: unchanged when round 2 is served by the SAME backend as the pin
    Method: test test/spec-review-window.test.mjs::govern: swap-reset fires from served-<n>.json even though the pin is still chain[0]
  • faff-1324#done:16 test/spec-review-occupant-served-dispatch.test.mjs:76-80 passes unmodified, asserting exit 0 and action: "swap-reset" through the real faff binary.
    Verified: Passes unmodified; git diff --name-only HEAD~1 HEAD lists no sibling test file.
    Method: test test/spec-review-occupant-served-dispatch.test.mjs::occupant dispatch (extracted verbatim from SKILL.md): round >= 2 idempotent no-op capture STILL writes served-<n>.json
  • faff-1324#done:17 test/spec-review-atomic-write.test.mjs:69,88 and test/spec-review-pin.test.mjs:262,290 pass unmodified.
    Verified: Both suites pass unmodified; 46/46 across the three sibling suites, and the diff touches none of them.
    Method: test test/spec-review-atomic-write.test.mjs::govern: a rename fault on governance-<n>.json throws and leaves no orphaned tmp file
    Method: test test/spec-review-pin.test.mjs::captureNoLensServed: a no-lens round after a served round PRESERVES the captured marker
    Method: test test/spec-review-pin.test.mjs::captureNoLensServed: a non-captured prior marker is replaced, not preserved
  • faff-1324#done:18 test/spec-review-window.test.mjs:268 (the prep prose lint) passes unmodified; git diff --stat shows no change to plugin/skills/faff-prep/SKILL.md and no change to SKILL_LINE_BASELINE.
    Verified: The prose lint passes and the diff touches neither plugin/skills/faff-prep/SKILL.md nor validate-adapters.js, so SKILL_LINE_BASELINE needs no bump.
    Method: test test/spec-review-window.test.mjs::faff-prep/SKILL.md: the loop resolves the window + round number via the new CLI
  • faff-1324#done:19 No change to spec-review-pin.js, spec-review-convergence.js, judge-trail.js, or any SKILL.md. git diff --name-only lists only spec-review-window.js and test/spec-review-window.test.mjs.
    Verified: git diff --name-only HEAD~1 HEAD lists exactly spec-review-window.js and its test; no change to spec-review-pin.js, spec-review-convergence.js, judge-trail.js or any SKILL.md.
    Method: command git diff --name-only HEAD~1 HEAD
  • faff-1324#done:20 The marker self-heal and the unguarded --backends-json failure write are filed as separate issues, each citing its extension point from OUT OF SCOPE.
    Verified: Both filed: FAFF‑1331 (the marker self-heal, citing capturePin's existing-pin arm at spec-review-pin.js:214-224 and the [n..n] latch consequence from Failure modes) and FAFF‑1332 (the unguarded --backends-json failure write, citing the atomicWriteJSON at spec-review-pin.js:382 inside the :378-385 catch block, and noting FAFF‑1052's never-silent property the fix must preserve). Each cites its OUT OF SCOPE extension point. Intake recorded for both.
    Method: human

Closes FAFF-1324

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Ready to land. Run this locally in a real terminal:

faff effects declare --run-dir .faff/anchors/run-20261017-042202-graft-FAFF-1324 --issue FAFF-1324 --step merge <<'EOF'
[{"kind":"merge","target":"pr:1087","reversible":true}]
EOF
faff merge-gate --pr 1087 --issue FAFF-1324 --run-dir .faff/anchors/run-20261017-042202-graft-FAFF-1324 --level L2 \
  --execute --merge-args "--squash --delete-branch"

…FAFF-1324)

The decision this spike was opened to settle: when pin-capture.json cannot
vouch for which round established the pin, govern() now takes the conservative
end of the unknown range. The else arm of the marker ternary sets capRound = n
instead of 1, and the late-pin rule reports unpinnable-reset when it fires with
an anomaly attached. Exit codes are unchanged, no field is added to
GovernanceResult, and no anomaly or action token is minted.

Two grounds, in order of weight. First, the fail-safe idiom for this exact
predicament is already settled twelve lines above: the pin === null arm narrows
to n and its comment calls that a fail-safe the governor MUST take. Four of the
six anomaly arms narrow to n, and prep narrows at both of its adjacent seams.
The marker arms were the only ones that widened by omission, and nothing
documented that asymmetry as intentional. Second, window_start is published as
the span the loop can vouch for; with capRound = 1 it was not. That is a
correctness claim about the artifact rather than about outcomes.

Be precise about the trade, because it is not a safer outcome. A wider window is
biased toward over-parking, never over-yielding: converging requires
strictly_decreasing across every consecutive pair and no_churn across every
step, both harder to satisfy as the window widens. Both dispositions therefore
terminate at needs-human. What this buys is a comparison the governor can
vouch for. What it costs is the one case where a correct yield was still
reachable, every round genuinely served by one reviewer, plus the remaining
rounds of the appetite cap. The status quo did not yield false convergence.

FAFF-1052 deferred this question rather than deciding it: the window-narrowing
disposition sits in its Proposed direction, under "Not settled, for prep to
shape", not in its Acceptance, and it shipped with capRound = 1 in a line it
wrote. This change does not claim to repair an acceptance breach.

The decision covers pin-marker-missing identically. The two are one ternary with
one severity and one fallback, and readCaptureOutcome returns null for an absent
and a torn marker alike, by documented design.

Tests: the two committed marker tests now pin action and window_start; three new
cases cover window_start already at n (never lowered), round 1 (inert), and an
unvouchable marker alongside a genuine swap (swap-reset, anomaly retained). Both
selftest twins assert the same, and the contradiction twin needed a window reset
because it shares a scratch dir with its sibling. The healthy late-pin-advance
path, the occupant-served-dispatch, atomic-write and pin suites all pass
unmodified.

Deferred, with extension points recorded in the spec: repairing the marker so
the anomaly stops latching, and the unguarded --backends-json failure write that
is a second live cause of a contradiction.

Signed-off-by: Alec Hill <[email protected]>
…ts served backend (FAFF-1324)

Review observation: the helper hardcoded winner_index 0 while served_identity came
from its servedBackend argument, so the swap fixture wrote BACKENDS[1]'s identity
beside index 0 and disagreed with its own data. govern() reads only served_identity,
so no test was weakened, but the record was internally inconsistent.

Signed-off-by: Alec Hill <[email protected]>
@alechill
alechill force-pushed the FAFF-1324-pin-marker-contradiction-disposition branch from 933caef to b6a1578 Compare October 17, 2026 13:42
@alechill
alechill merged commit 3501de7 into main Oct 17, 2026
14 checks passed
@alechill
alechill deleted the FAFF-1324-pin-marker-contradiction-disposition branch October 17, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant