Skip to content

fix(invariants): complete the #477 gate-19 demotion in the two suites it missed, and bound _warn by name - #481

Merged
rubenvdlinde merged 2 commits into
mainfrom
S38/package-invariants
Aug 16, 2026
Merged

fix(invariants): complete the #477 gate-19 demotion in the two suites it missed, and bound _warn by name#481
rubenvdlinde merged 2 commits into
mainfrom
S38/package-invariants

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Package invariants on .github@main has been RED since 13:15, and the broken property is not the one it looked like

Measured against main = 6f756a8b (hydra-gates package run 31950603565, step All helper suites (discovered, not listed): passed: 90, quarantined: 2, failed: 2).

Two assertions fail. Neither of them is the empty-scope property. That property — no gate reports PASS over a diff that excludes every planted defect — passes on main today and passes here, with its own positive control (18 gates FAIL the planted tree at full scope) intact before and after.


Which PR caused it — settled by running the tests, not by reading CI

The bisection was open between #476 and #478 because #476's run was cancelled, and cancelled is neither passed nor failed. (Cause: the workflow's concurrency: cancel-in-progress: true on hydra-gates-package-refs/heads/main; #476's run started 13:15:37, #478's 13:15:44 — seven seconds later, same group.)

So the two suites were run directly against each merge commit's tree, which needs no CI at all:

tree what it is empty_scope_never_passes discarded_counts_and_empty_deltas
ff66c3b pre-#476 0 failures 0 failures
0b2c457 #476 merged (gate-35 + gate-25) 0 failures 0 failures
530a849 #478 merged (gate-19 advisory) 1 failure 1 failure
6f756a8 main (#479 merged) 1 failure 1 failure

#476 is exonerated. The culprit is #478 (e1fe8a4, fix/gate-19-advisory). The two locally-reproduced assertions are byte-identical to the two in the CI log.

#478 taught three sibling suites the new verdict word in the same commit — test_gate_exit_code_semantics.sh, test_gate_base_ref_delivery_channel.sh, and the acceptance matrix with its prose-not-proof/expect.conf. It missed these two, because its TESTS section names the two suites it ran by hand. That is the hand-maintained-list decay tests/run-helper-suites.sh exists to prevent — the runner caught it, one merge later.


What each assertion asserts, and what it got

1 — test_gate_empty_scope_never_passes.sh, ARM 1

  • asserts: [gate-19] …: FAIL over a fixture carrying one planted unannotated scenario.
  • got: [gate-19] e2e-coverage: WARNING — 1 scenario(s) missing @e2e (advisory, non-blocking — see .github#477)

ARM 1 is the positive control for the fixture — every arm after it is meaningless unless the gate demonstrably sees the planted defect. #477 moved gate-19's verdict word, not its detection. The arm matched the word rather than the property.

2 — test_gate_discarded_counts_and_empty_deltas.sh, ARM P3

Its stated property is "the two parsers must stay in step"; the mechanism it implemented froze the vocabulary. Its own repair path was to retype a literal that nobody can check against the files it polices.


Which side is wrong

The tests — in mechanism only, and the repair is a strengthening, not a relaxation. Relaxing this suite to get main green would be the invisible pass, in the one place that guards against invisible passes, so nothing here weakens a verdict:

  • ARM 1 accepts FAIL or WARNING for gate-19 and now additionally requires the line to name a non-zero count — a demotion's real failure mode is the finding quietly ceasing to be read. PASS and NOT APPLICABLE are still refused, and ARM 2 separately requires NOT APPLICABLE over the empty scope, so the two stay distinguishable. gate-25 is not demoted and keeps the strict FAIL — accepting WARNING for a gate nobody demoted would itself be the defect.
  • New: the run summary must announce the advisory findings and state that green means "nothing BLOCKING failed".
  • ARM P3 now extracts the alternation each parser actually uses and requires them byte-identical, with the four core verdict forms as a floor so "agreement" cannot be reached by gutting both. Vocabulary may grow; it may not grow on one side only.
  • New ARM P4 — which gates may be advisory is bounded by name. _warn (new in fix(gate-19): demote e2e-coverage to advisory while the fleet is repaired #478) is one line from being a soft-fail switch for any gate in the suite, and nothing in this package would have noticed a second one landing. The allowlist is 19 (.github#477), and demoting another gate now means editing this test and stating why. This does not stop a demotion; it stops a silent one.

The assertion floor in the discarded-counts suite moves 34 → 37 to match, so an arm cannot be deleted and still read green.


Positive controls — proof each assertion can still fail

Each mutation applied to an unmodified copy of the package, then the suite run against that copy:

mutation result
gate-19 _warn_pass 19 "e2e-coverage" ARM 1 FAILSgate-19 did NOT catch its planted true positive — got: PASS
gate-19 warning kept, count removed ARM 1 FAILSnamed no non-zero scenario count
WARNING added to one parser only ARM P3 FAILS, naming both alternations
gate-18's advisory turned into _warn 18 ARM P4 FAILSfound '18 19', allowed '19'

Both new arms also carry their controls inline, so a future extractor that returns empty for everything cannot call that agreement.

Before / after

suite main @ 6f756a8b this branch
test_gate_empty_scope_never_passes.sh 1 failure (exit 1) ALL PASS (exit 0)
test_gate_discarded_counts_and_empty_deltas.sh 1 failure (exit 1) 0 failures, 37 assertions (exit 0)
run-helper-suites.sh (94 discovered) 90 passed / 2 quarantined / 2 failed 92 passed / 2 quarantined / 0 failed

Two files changed, both under hydra-gates/scripts/lib/, both test_*. No gate, no checker and no runner behaviour is modified — the blast radius on the 18 consuming repos is zero.

Not done, deliberately


🔴 And a third finding, which was NOT red and is the worse of the two

There are three verdict parsers in this package, not two. #477 taught gf_verdict and the acceptance matrix the new WARNING form. It left _verdict_set in test_gate_base_ref_delivery_channel.sh — the parser that decides "every gate returned the same verdict through both channels" — matching only (PASS|FAIL|NOT APPLICABLE|SKIPPED).

Nothing went red, and that is the defect. A verdict word that pattern does not know is dropped, not flagged. gate-19 fell out of both channels' verdict sets, the comparison went on matching over a set that no longer contained the gate that suite exists to watch, and it reported 12 passed / 0 failed.

main @ 6f756a8b this branch
base-ref channel parity every gate returned the same verdict … (**69** gate(s) compared) … (**70** gate(s) compared)

Positive control — an A/B against a real leak, not an argument

A copy of the runner was mutated to inflate gate-19's count by one when the base arrives through $HYDRA_GATE_BASE_REF rather than --base — i.e. exactly the .github#416 defect this suite exists to catch, planted in gate-19. Both copies are otherwise identical; only the suite differs:

suite under test verdict
as it is on main today PASS12 passed, 0 failed, (69 gate(s) compared)
with this branch FAILTHE DELIVERY CHANNEL CHANGED THE VERDICT

The main-today arm prints both arms' differing counts — WARNING — 1 scenario(s) and WARNING — 2 scenario(s)two lines above the assertion that then declares them identical.

ARM P3 now compares all three parsers by name and requires one shared alternation, so a fourth parser, or a fourth verdict word added to only some of them, cannot land quietly.

Revised file count

Three files, all hydra-gates/scripts/lib/test_* / gate_fixture_support-adjacent test code. No gate, checker or runner behaviour is modified — blast radius on the 18 consuming repos is still zero.

Conduction Release Bot added 2 commits August 16, 2026 17:56
…ed, and bound it

`.github@main`'s `Package invariants` job has been red since 13:15 today. Both
failures come from #478 (`e1fe8a4`, gate-19 demoted to advisory), and NEITHER of
them is the empty-scope property itself — that property still holds and is still
asserted.

WHICH PR, SETTLED BY RUNNING THE TESTS RATHER THAN READING CI
#476's CI run was CANCELLED (concurrency `cancel-in-progress`, #478 pushed 7s
later), so it was neither passed nor failed and could not be exonerated from CI.
The two suites were run directly against each merge commit's tree instead:

  ff66c3b  pre-#476                     both suites GREEN
  0b2c457  #476 merged (gate-35/25)     both suites GREEN   <- exonerated
  530a849  #478 merged (gate-19)        both suites RED, 1 assertion each
  6f756a8  main (#479 merged)           both suites RED, same two assertions

WHAT EACH ASSERTION ASSERTED, AND WHAT IT GOT

1. test_gate_empty_scope_never_passes.sh, ARM 1
   asserted: `[gate-19] ...: FAIL` over a fixture carrying one planted
             unannotated scenario.
   got:      `[gate-19] e2e-coverage: WARNING — 1 scenario(s) missing @e2e`.

   ARM 1 is the POSITIVE CONTROL for the fixture — everything after it is
   meaningless unless the gate demonstrably sees the planted defect. #477
   deliberately moved gate-19's verdict word, not its detection. The arm matched
   the word rather than the property.

2. test_gate_discarded_counts_and_empty_deltas.sh, ARM P3
   asserted: the literal `(PASS|FAIL|NOT APPLICABLE|SKIPPED)` appears in BOTH
             gate_fixture_support.sh and test_gate_acceptance_matrix.sh.
   got:      both files now read `(PASS|FAIL|WARNING|NOT APPLICABLE|SKIPPED)` —
             changed IN STEP by #478, exactly as their comments require.

   The stated property is "the two parsers must stay in step". The implemented
   mechanism froze the vocabulary instead, so a correct in-step change failed
   it, and its own repair path was to retype a literal nobody can check against
   the files it polices.

WHICH SIDE WAS WRONG
The tests, on both counts — but only in mechanism, and the repair strengthens
rather than relaxes:

- ARM 1 now accepts FAIL *or* WARNING for gate-19 AND requires the line to name
  a non-zero count, because a demotion's real failure mode is the finding
  ceasing to be read. gate-25 is not demoted and keeps the strict FAIL. PASS and
  NOT APPLICABLE are still refused, and ARM 2 still requires NOT APPLICABLE over
  the empty scope, so the two remain distinguishable.
- A new assertion requires the run SUMMARY to announce the advisory findings and
  to say that green means "nothing BLOCKING failed".
- ARM P3 now extracts the alternation from each parser and requires them to be
  byte-identical, with the four core verdict forms as a floor so "agreement"
  cannot be reached by gutting both.
- NEW ARM P4: the set of gates allowed to call `_warn` is an explicit allowlist
  (`19`, .github#477). `_warn` is otherwise one line away from being a soft-fail
  switch for any gate in the suite, and nothing in this package would have
  noticed a second one. This does not stop a demotion; it stops a silent one.

The empty-scope property is untouched. ARM 2 (`NOT APPLICABLE`, never PASS) and
ARM 6 (`no gate reports PASS over a diff that excludes every planted defect`,
positive control: 18 gates FAIL the planted tree at full scope) pass unchanged
before and after.

POSITIVE CONTROLS — each mutation run against an unmodified copy of the package
  gate-19 `_warn` -> `_pass 19`            ARM 1 FAILS: "got: PASS"
  gate-19 warning with the count removed   ARM 1 FAILS: "named no non-zero count"
  WARNING added to ONE parser only         ARM P3 FAILS, naming both alternations
  gate-18's advisory turned into `_warn`   ARM P4 FAILS: "found '18 19', allowed '19'"
Both suites also carry the drift/allowlist controls inline, so a future
extractor that returns empty for everything cannot call that agreement.

The assertion floor in the discarded-counts suite moves 34 -> 37 to match.

NOT DONE, DELIBERATELY
#478's demotion of gate-19 is a fleet policy decision with a stated reason and a
tracking issue; reverting it is the coordinator's call, not this PR's. gate-18's
advisory line is printed AFTER its verdict, so `head -1` parsers are safe today
— but it is now shape-matchable as a verdict, and that safety is an ordering
rather than a mechanism.
…nd it fails silently

There are three verdict parsers in this package, not two. #477 taught
`gf_verdict` and the acceptance matrix the new WARNING form. It left
`_verdict_set` in test_gate_base_ref_delivery_channel.sh — the one that decides
"every gate returned the same verdict through both channels" — matching only
(PASS|FAIL|NOT APPLICABLE|SKIPPED).

Nothing went red, and that is the defect. A verdict word this pattern does not
know is DROPPED, not flagged. So gate-19 fell out of both channels' verdict
sets, the comparison went on matching over a set that no longer contained the
gate that suite exists to watch, and it reported 12 passed / 0 failed.

MEASURED, on the real fixture:
  main @ 6f756a8   "every gate returned the same verdict ... (69 gate(s) compared)"
  this branch       "every gate returned the same verdict ... (70 gate(s) compared)"

POSITIVE CONTROL — an A/B against a REAL leak, not an argument.
A copy of the runner was made to inflate gate-19's count by one when the base
arrived through the environment rather than through --base, i.e. exactly the
.github#416 defect this suite was built to catch, planted in gate-19:

  suite as on main today   PASS — 12 passed, 0 failed, "(69 gate(s) compared)"
  suite with this commit   FAIL — "THE DELIVERY CHANNEL CHANGED THE VERDICT"

The main-today arm prints both arms' differing counts (`WARNING — 1` vs
`WARNING — 2`) two lines above the assertion that then declares them identical.

Both this and the ordinary case are now mechanised: ARM P3 of
test_gate_discarded_counts_and_empty_deltas.sh compares ALL THREE parsers by
name and requires one shared alternation, so a fourth parser or a fourth verdict
word cannot be added to some of them.
@rubenvdlinde
rubenvdlinde merged commit da89759 into main Aug 16, 2026
36 checks passed
@rubenvdlinde
rubenvdlinde deleted the S38/package-invariants branch August 16, 2026 16:22
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