Skip to content

fix(gate-28): an empty diff scope is not a structural gap — it is ADR-020 working - #182

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-28-empty-scope-is-not-a-gap
Aug 6, 2026
Merged

fix(gate-28): an empty diff scope is not a structural gap — it is ADR-020 working#182
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-28-empty-scope-is-not-a-gap

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

gate-28 goes RED on every PR whose diff does not touch lib/**/*.php — which is most of them. It reports the diff's shape as the repository's defect.

Found while unpinning nldesign to main per #177: with the pin removed both capability probes pass, all 58 applicable gates go green, and the only failure is gate-28 not running. It does not exist at v1.3.0, so unpinning is what surfaced it — and it would have turned the whole fleet red as the sweep landed.

The conflation

_lt_files empty had two causes, and the final else stated them as one:

  • (a) the repo has lib/**/*.php but this diff touches none — ADR-020 diff-scoping working, the same state gate-4 reports NOT APPLICABLE for the same diff;
  • (b) lib/ exists but holds no tracked .php at all.

Neither is (c) "files were in scope and none carried a declaration" — the genuine structural gap that branch describes. All three printed (c), and for (a) that message is simply false: with zero files in scope, "0 in-scope file carried a declaration" is true only vacuously.

Not cosmetic: hydra-gates-require-full-coverage defaults TRUE (#164) and a structural gap exits 98. This is a fleet-wide falsely-RED gate — the mirror of the falsely-GREEN shape #172 built this taxonomy to kill.

Three arms, measured on nldesign against the real runner

diff before after
.github/workflows/code-quality.yml SKIPPED (structural) → exit 98 NOT APPLICABLE
3 files under lib/ PASS PASS
one lib/*.php declaring AGPL while composer.json declares EUPL-1.2 FAIL — 1 file(s)

The third arm is the one that matters. Turning a red into a not-applicable is exactly how a gate gets muted, so the mismatch gate-28 exists to catch was injected on a throwaway branch and confirmed still caught.

The (c) message now also reports the in-scope file count, so a reader can tell it apart from the case it used to be conflated with.

…-020 working

gate-28 turned RED on every PR in the fleet whose diff does not touch
lib/**/*.php, which is most of them. It reported the DIFF's shape as the
REPOSITORY's defect.

`_lt_files` empty had two causes, and the final `else` stated them as one:

  a) the repo has lib/**/*.php but this diff touches none of them — ADR-020
     diff-scoping working, the same state gate-4 reports NOT APPLICABLE for
     the same diff;
  b) lib/ exists but holds no tracked .php at all.

Neither is (c) "files WERE in scope and none carried a declaration", the
genuine structural gap that branch describes. All three printed (c), and for
(a) that message is FALSE: with zero files in scope, "0 in-scope file carried
a declaration" is true only vacuously.

Not cosmetic. hydra-gates-require-full-coverage defaults to TRUE (#164) and a
structural gap fails the run with exit 98, so this was a fleet-wide falsely-RED
gate — the mirror of the falsely-GREEN shape #172 built this taxonomy to kill.
It surfaced the moment a repo unpinned to main, because gate-28 does not exist
at v1.3.0.

Measured on nldesign against the real runner, three arms, before and after:

  diff = .github/workflows/code-quality.yml   before SKIPPED(structural) -> exit 98
                                              after  NOT APPLICABLE
  diff = 3 files under lib/                   before PASS   after PASS
  diff = one lib/*.php declaring AGPL while
         composer.json declares EUPL-1.2      after  FAIL — 1 file(s)

The third arm is the one that matters: turning a red into a not-applicable is
exactly how a gate gets muted, so the mismatch it exists to catch was injected
and confirmed still caught.

The (c) message now also states the in-scope count, so a reader can tell it
apart from the case it used to be conflated with.
@rubenvdlinde
rubenvdlinde merged commit da23b92 into main Aug 6, 2026
29 checks passed
rubenvdlinde pushed a commit to ConductionNL/nldesign that referenced this pull request Aug 6, 2026
…it was cut

development moved from v1.3.0 to v1.5.0 while this branch was open, which
fixes the symptom that opened it: v1.5.0 satisfies #177's capability probe,
so Hydra Gates no longer fails before a gate runs. Unpinning entirely still
matters, and the reason is now sharper than when this branch started.

v1.5.0 carries a fleet-wide falsely-RED gate. gate-28 license-triangle
reported a `structural` coverage gap whenever the diff scope was empty —
that is, on every PR that does not touch lib/**/*.php, which is most of
them — because "no file in scope" and "files in scope, none carried a
licence declaration" reached the same branch. With
hydra-gates-require-full-coverage defaulting TRUE, that is exit 98.

It is fixed in ConductionNL/.github#182, on main. A repo pinned to v1.5.0
cannot see that fix, and the tag will not move. Measured here against the
real runner, three arms:

    diff = .github/workflows/code-quality.yml   before SKIPPED(structural)
                                                after  NOT APPLICABLE
    diff = 3 files under lib/                   PASS, both
    diff = a lib/*.php declaring AGPL while
           composer.json declares EUPL-1.2      FAIL — 1 file(s)

That is the whole argument for #177's contract in one branch: this repo was
pinned at v1.3.0, bumped to v1.5.0 to clear one breakage, and would have
needed a third commit to clear the next one. A pin is a silent expiry date
on every upstream fix.

Verified on this PR's own run after the unpin: Hydra Gates PASS in 20s,
58 applicable gates green, 5 correctly NOT APPLICABLE.

Rollback for this repo alone stays available: set hydra-gates-ref
explicitly. An escape hatch, not a resting state.
rubenvdlinde pushed a commit to ConductionNL/nldesign that referenced this pull request Aug 6, 2026
…it was cut

development moved from v1.3.0 to v1.5.0 while this branch was open, which
fixes the symptom that opened it: v1.5.0 satisfies #177's capability probe,
so Hydra Gates no longer fails before a gate runs. Unpinning entirely still
matters, and the reason is now sharper than when this branch started.

v1.5.0 carries a fleet-wide falsely-RED gate. gate-28 license-triangle
reported a `structural` coverage gap whenever the diff scope was empty —
that is, on every PR that does not touch lib/**/*.php, which is most of
them — because "no file in scope" and "files in scope, none carried a
licence declaration" reached the same branch. With
hydra-gates-require-full-coverage defaulting TRUE, that is exit 98.

It is fixed in ConductionNL/.github#182, on main. A repo pinned to v1.5.0
cannot see that fix, and the tag will not move. Measured here against the
real runner, three arms:

    diff = .github/workflows/code-quality.yml   before SKIPPED(structural)
                                                after  NOT APPLICABLE
    diff = 3 files under lib/                   PASS, both
    diff = a lib/*.php declaring AGPL while
           composer.json declares EUPL-1.2      FAIL — 1 file(s)

That is the whole argument for #177's contract in one branch: this repo was
pinned at v1.3.0, bumped to v1.5.0 to clear one breakage, and would have
needed a third commit to clear the next one. A pin is a silent expiry date
on every upstream fix.

Verified on this PR's own run after the unpin: Hydra Gates PASS in 20s,
58 applicable gates green, 5 correctly NOT APPLICABLE.

Rollback for this repo alone stays available: set hydra-gates-ref
explicitly. An escape hatch, not a resting state.
rubenvdlinde added a commit to ConductionNL/zaakafhandelapp that referenced this pull request Aug 6, 2026
…it was cut (#331)

This repo has been bumped twice already (v1.0.1 -> v1.3.0 -> v1.5.0), each
time to clear a breakage the pin itself caused, and a third was queued.

v1.5.0 ships gate-28 reporting a `structural` coverage gap whenever the diff
scope is empty — i.e. on every PR that does not touch lib/**/*.php, which is
most of them. With hydra-gates-require-full-coverage defaulting TRUE that is
exit 98. It is fixed in ConductionNL/.github#182, on main, and a pinned repo
cannot see that fix because the tag will not move.

Tracking main is the fleet contract as of ConductionNL/.github#177, which
also added the guard that makes it safe: an unresolvable reusable workflow
produces no red check at all, just zero jobs, so quality-resolve-probe.yml
counts the jobs a caller materialises and asserts > 0.

Supersedes #329, which set v1.4.0 — a DOWNGRADE from what development now
carries, and to a ref that fails #177's capability probe outright (v1.4.0
ships none of check_spec_anchors.py, check_form_labels.py or
check_license_triangle.py).

Rollback for this repo alone stays available: set hydra-gates-ref explicitly.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Aug 8, 2026
…s missing (#248)

#178 (gate-28 fails every PHP-free PR in a PHP repo) was already FIXED by
#182 on 2026-08-06, released in v1.5.2 and v1.6.0. The issue was simply
never closed. Measured, not assumed — all three states behave correctly on
main today. This commit adds no runtime change; it adds the acceptance
evidence and closes three real gaps in what the suites actually asserted.

WHAT WAS NOT BEING ASSERTED

1. The suite never ran --require-full-coverage, so it proved "gate-28 prints
   NOT APPLICABLE" and INFERRED "the run is not failed". Those are different
   claims and have come apart before (#164 defaulted the flag TRUE in the
   workflow while every local suite ran without it).

2. Nothing exercised the _fail 28 branch end to end. A licence MISMATCH is
   the gate's actual purpose; only the weaker "no file carried a tag"
   control existed. The helper->log->wc -l->_fail wiring was untested.

3. check_license_triangle.py's declared_file_count() had NO test in either
   direction, despite being the single number the runner's PASS-vs-structural
   decision rests on, and nothing pinned the stdout/stderr split it travels
   over.

A TRAP FOUND WHILE WRITING THIS

The coverage-failure branch lives inside `if [ "${_FAILED}" -eq 0 ]`. Any
other failing gate suppresses the sentence entirely, so "the coverage
sentence is absent" passes automatically on a fixture that trips an
unrelated gate — including on a fully reverted runner. The first draft wrote
its Vue file to src/App.vue, which gate-38 fails; both runs exited 1, not 98,
and both new assertions were measuring gate-38. Fixtures are now otherwise
green (src/components/Widget.vue), the pass case reads the POSITIVE "ALL N
APPLICABLE GATES GREEN" sentence, and the control uses a composer.json with
no license field rather than untagged PHP (which also fails gate-1, making
the coverage branch unreachable).

MUTATION-CHECKED — every new assertion shown capable of failing:
  M1 empty diff scope -> structural (pre-#182)  -> 2 new shell assertions RED, status 98
  M2 structural -> na (gate stops failing)      -> 2 new shell assertions RED
  M3 _fail 28 disabled                          -> 2 new shell assertions RED (drift became PASS)
  M4 declared_file_count -> len(files)          -> 3 new python tests RED
  M5 count printed to stdout                    -> 4 new python tests RED; clean repo turns FAIL
  M6 empty scope invents a finding + a count    -> 3 new python tests RED
  M7 declared_file_count -> 0                   -> 5 new python tests RED; PASS became structural

Counts: shell 5 -> 11 assertions, python 19 -> 29 tests. Both appended to,
never overwritten. Full helper suite: 27 passed, 0 failed, 2 quarantined.

No runtime file is touched: the diff is the two test files only.

Closes #178

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
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