Skip to content

DO NOT MERGE — mutation proof for #3617: the milestone gate call deleted - #3620

Closed
noahgift wants to merge 3 commits into
mainfrom
MUTATION-PROOF-3459-do-not-merge
Closed

noahgift wants to merge 3 commits into
mainfrom
MUTATION-PROOF-3459-do-not-merge

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

Throwaway, draft, never merged. Evidence for #3617 condition (b).

One line deleted: the check_milestone_cut.sh call inside cut_tag(). Expectation: guard-tree RED (the behavioural guard refuses), therefore gate RED — gate reads needs.guard-tree.result != "success" and is a required context via ruleset 13878864.

The mutant still contains the string check_milestone_cut.sh three times, in cut_tag()s own die messages — a grep-based guard would pass this file. Only running cut_tag() against a stubbed gate catches it:

ok    gate rc=0 -> the tag is cut
FAIL  gate rc=1 (milestone holds open items) -> A TAG WAS CUT ANYWAY
FAIL  gate rc=2 (Unknown) -> A TAG WAS CUT ANYWAY

That is the v0.68.1 condition reconstructed. Deleted once cited in #3617.

keep-open: throwaway evidence branch, closed by hand once #3617 cites the run. Refs #3459, #3617.
ont-delta: none — a throwaway mutation branch, never merged.

noahgift and others added 3 commits September 20, 2026 19:00
Closes the exit criterion #3459 is actually about. v0.68.1 was tagged 2026-09-17
15:06:29Z by an autopilot copy living OUTSIDE the repository, six minutes after
#3455 merged the milestone gate; `grep -c check_milestone_cut autopilot.sh` was 0.
The spec required the read and no code path that cuts a tag made it.

THE GATE IS INSIDE THE FUNCTION THAT TAGS. cut_tag() calls
scripts/check_milestone_cut.sh ahead of `git tag`, and there is no path through it
that reaches `git tag` with the gate unsatisfied. Both non-zero codes are fail-closed
and they are DIFFERENT failures, named separately:

  rc 0  -> MILESTONE-GATE <v> clean at the cut; the tag is cut
  rc 1  -> the milestone holds open item(s): no tag, no publish
  rc 2+ -> the gate could not judge: no tag. Unknown is not a pass.

THE GUARD IS BEHAVIOURAL, NOT A grep. "The file mentions the gate" is satisfied by a
comment. scripts/check_tag_step_gated.sh EXTRACTS cut_tag() from the autopilot, runs
it against a stubbed gate once per outcome, and asserts on whether a GIT-TAG appears
in the transcript. Its --self-test then builds three mutants and requires each to be
RED, and the real subject to be GREEN so a red subject cannot masquerade as a killed
mutant:

  mutant 1  gate call deleted (the #3459 defect, restored)  -> RED, 2 failing rows
  mutant 2  gate verdict discarded with `|| true`           -> RED
  mutant 3  cut_tag() removed entirely                      -> ENV rc=2, never a pass

That covers #3459's three required mutations: gate call removed => RED, gate exit 1
=> no tag and no publish, gate exit 2 => no tag.

contracts/tag-step-milestone-gate-v1.yaml (kind: pattern) lands in the same PR, per
the ticket. `pv validate` and `pv lint` both PASS, and every one of its six
falsification tests was RUN, not assumed -- which is how FALSIFY-TSMG-006 was caught
asserting `test -x` on a file the repo deliberately keeps non-executable (guard_tree.sh
invokes its whole universe as `bash scripts/...`, so the executable bit is not the
property). The test was wrong, not the file; corrected to `test -f`.

WIRING, with no .github/workflows edit and no operator check-in: guard_tree.sh's
universe is exactly `git ls-files 'scripts/check_*.sh'`, a flat glob, so the guard runs
in the guard-tree job -- and `ci / gate` reads needs.guard-tree.result, so it is gated
by the required check transitively. #3459 notes that wiring a self-test literally inside
the `ci` job would be a workflow edit; this route does not need one.

SEC011 ("missing validation for 'd' before rm -rf") was real in the new guard and is
fixed with ONE validated rmtree() used by every call site, not suppressed and not
repeated per site.

Gates, fresh worktree off origin/main 00e5d2d -- all 12 rc=0:
  check_tag_step_gated.sh and --self-test · check_hardcoded_paths.sh --full-if-capable
  check_bashrs_gate.sh and --self-test · check_apr_bin_pinned.sh
  check_no_pipe_into_grep_q.sh · check_shell_lint_ratchet.sh · check_guards_are_wired.sh
  check_contract_enforcement.sh · check_contract_test_binding.sh
  check_sourced_libs_option_neutral.sh

NOT in this PR, deliberately: V/T/MS/EPIC and AP are still 0.68.2's. Parameterising
them is its own row -- mixing it here would make the gate change unreviewable, which is
the lesson this ticket has been teaching all day. The hazard is stated in the PR body:
the hardcoded-path ratchet does NOT match /mnt (#3592, proved with a committed
two-path probe scoring +1 not +2), so AP=/mnt/... passes every gate we own and must be
changed by hand with nothing to remind anyone.

Refs #3080, #3445, #3455, #3592

Pmat-Ticket: PMAT-3459
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found while answering #3459's exit criterion 4. The self-test added in the previous
commit was DISCOVERED BY NOTHING and therefore ran in no job at all:

  before:  PASS  scripts/check_tag_step_gated.sh [run]
  after:   PASS  scripts/check_tag_step_gated.sh [self-test]
           PASS  scripts/check_tag_step_gated.sh [run]

guard_tree.sh decides a guard "advertises --self-test" by looking for the literal
substring `self-test` in that guard's OWN `--help` output (guard_tree.sh:47,
advertises_self_test). check_tag_step_gated.sh had no --help handler, so it got one
row instead of two and its three mutants were never executed by CI. The mutation
proof existed and nothing ran it -- a facility with a self-test and no caller, in the
commit that added the self-test. It is the same class this ticket keeps producing,
and it was invisible to reading: the guard works, its self-test works, and the wiring
between them did not exist.

The fix is a --help handler printing the header comment, which is the idiom the rest
of the tree uses (ci_run_explicit_test_commands.sh:147 and others).

Verified by running guard_tree.sh --no-cargo, not by reading it: 66 checks, 0 failed,
with both rows present.

Refs #3080, #3459

Pmat-Ticket: PMAT-3459
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t_tag()

Throwaway branch for #3617's condition (b). Deletes ONE line -- the
check_milestone_cut.sh invocation inside cut_tag() -- and nothing else, to show
the named required context go RED on a real run rather than only in a local
--self-test.

Note the mutant still contains the string 'check_milestone_cut.sh' THREE times,
in cut_tag()'s own die messages. A grep-based guard would pass this file. Only
the behavioural guard, which runs cut_tag() against a stubbed gate, catches it:

  ok    gate rc=0 -> the tag is cut
  FAIL  gate rc=1 (milestone holds open items) -> A TAG WAS CUT ANYWAY
  FAIL  gate rc=2 (Unknown) -> A TAG WAS CUT ANYWAY

That is the v0.68.1 condition, reconstructed.

This branch is never merged and is deleted once the run is cited.
@github-actions

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3620 head=933b7f1bb05ca531e227db77dab0af8c7c6b1841 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

@noahgift

Copy link
Copy Markdown
Contributor Author

Mutation proof complete and cited on #3617 (guard-tree → gate, both RED on this head; ci / gate green). Throwaway; closing and deleting the branch.

@noahgift noahgift closed this Sep 20, 2026
@noahgift
noahgift deleted the MUTATION-PROOF-3459-do-not-merge branch September 20, 2026 21:10
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