fix(gates): seven of gates 23-33 could not fail on their own subject matter - #276
Merged
Conversation
…matter
Acceptance test applied to every gate in the band: plant ONE textbook true
positive in a real fleet repo, run the gate, require it to FAIL and NAME the
defect, remove the plant, require the prior verdict back, and keep a clean
sibling passing so no repair is a widening.
Eight gates already passed that test unchanged (25, 26*, 27*, 28, 29*, 31, 32,
33*) — the starred ones only for the shape that was planted. Seven could not:
gate-23 The linter is bake-in gated: in WARN mode it exits 0 whether it
matched nothing or everything, and the gate read only the byte. A
planted lib/Service/TenantIsolationService.php on doriath produced
`[gate-23] …: PASS`, byte-identical to a clean repo. Worse, its
`grep -v -i openregister` exclusion reads the FILE PATH, which inside
the openregister repository never contains that string: 33 findings
there, every one a canonical OpenRegister class being told to consume
itself, and the linter starts BLOCKING on 2026-08-13.
Fixed: identity by appinfo/info.xml `<id>`, not `basename $(pwd)`; the
ADR-022 "consume the OR abstraction" rules do not apply to the repo
that PROVIDES those abstractions (announced, never silent); the linter
emits `or_abstraction_findings=<n>` and the gate STATES the count and
surfaces the matched rules even when it cannot block. Measured on
openregister: 35 matched paths -> 1 (the PDOK rule, whose provider is
openconnector, still applies and still fires).
gate-24 openregister's parity wrapper exits 0 printing "canonical JS check not
found locally — skipping". The gate read that 0 as a PASS, so the repo
that owns the integration registry correlated nothing and said it was
fine. A self-declared skip is now classified on the gate's own subject
matter — na when no leaves are registered, structural when they are.
(hermiq's wrapper is the honest shape and still fires: renaming its
registerIntegration id produced 2 R2 id-correlation violations.)
gate-26 check_visual_coverage.py returned the FINDING COUNT as its exit status
and the gate read that byte as the count. Measured on openregister:
260 uncovered new page components -> exit 4 -> "FAIL — 4"; 256 ->
exit 0 -> "[gate-26] visual-coverage: PASS", with the helper's own
stdout reading "FAIL — 256" on the same run. This is .github#209 in a
second gate. The helper now returns 0/1/2/3 like gate-25's and puts
the count on stdout; the gate reads it there, keeps stderr, calls an
empty scope `na` and a crash `wiring`. It also stopped defaulting
HYDRA_GATE_BASE_REF (#242), so a full-repo run reaches it — doriath
has 10 uncovered page components CI has never seen.
gate-27 `2>/dev/null || true` around a helper that returns 0 on every
successful path: its only nonzero exit is a crash, and a crash gave an
empty log and a PASS. Measured: 404 files in scope, none judged,
verdict PASS. Now a wiring skip with the traceback kept. An empty file
set is `na` rather than PASS.
gate-29 A bare `[ fail -eq 0 ] && PASS` reachable by both guards, so an
unscoped run and a repo with no .gitignore printed the same PASS as a
run that inspected a real .gitignore change. Now na, with the checked
rule count stated on a genuine pass. The ignore prefix is also escaped
before being used as an ERE — `.phpunit.cache` was a pattern, not data.
gate-30 A route named `AppHost\Controller\GenericHealth#index` maps by PSR-4
to lib/Controller/AppHost/Controller/…, but openregister DI-binds it
to lib/AppHost/Controller/GenericHealthController.php. The gate called
the file "not present in this repository" — inside the repository that
contains it — so /api/health and /api/metrics were judged nowhere in
the fleet. Now resolved by basename under lib/ when, and only when,
exactly one tracked file matches. The healthPing carve-outs are
unchanged and asserted: a per-object GET and a POST are still not
scrape targets, because the only remedy for flagging them is
publishing an outbound-ping oracle.
gate-33 A report containing `{}` parsed as "no violations" and reported PASS,
turning the loud missing-report skip into a silent false green. A JSON
file with no `violations` key is not an axe result object — axe-core
always emits it — and is now a wiring skip. A real report prints how
many violations a PASS is a pass over.
Also, in the shared provenance filter that gates 5/9/30 depend on:
filter_preexisting_methods.py compared method BODIES starting at the
`function NAME(` line, so deleting `#[PublicPage]` left the body byte-identical
and the finding was moved out of the gate log. Measured on openregister with
the attribute removed from its own GenericHealthController in the diff under
test: findings log empty, `.preexisting` holding the finding, gate-30 PASS.
The comparison now includes the attribute/docblock region, which can only move
entries back INTO the findings log.
Gates 31 and 32 kept their rules; both now report `na` instead of PASS when no
markup file is in scope, and state how many files a PASS covers.
Tests (all mutation-checked against the pre-fix files):
scripts/lib/test_gates_23_33_never_green_over_nothing.sh — 39 assertions over
a planted/clean fixture pair plus a crashed-checker arm; 12 fail against
main's checkers.
scripts/lib/test_check_visual_coverage.py — 10 assertions incl. the 256-case
whose low byte is zero; 6 fail against main's helper.
test_filter_preexisting_methods.py — attribute-removal case (fails on main)
plus its anti-widening sibling.
tests/test-hydra-gates-bin.sh — its "src/ exists so every src-guarded gate
runs" control now supplies real markup and a real page component, because
`[ -d src ]` was never gates 26/31/32's subject matter and asserting PASS
over a src/ holding one .js file is the nldesign shape it exists to stop.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
…o lib/Controller/
NC's RouteParser::buildControllerName() does not prefix the app namespace when
the route name already contains a backslash, so
`AppHost\Controller\GenericHealth#index` is looked up as the bare class
`AppHost\Controller\GenericHealthController`. PSR-4 maps `OCA\<App>\` onto
`lib/`, so that class lives at lib/AppHost/Controller/ — not under
lib/Controller/, which is the only place the resolver looked.
Reproduced against this package's own gates-23-33/planted fixture:
lib/Controller/AppHost/Controller/GenericHealthController.php
route='AppHost\Controller\GenericHealth#index'
rule=controller-class-not-found
a path that cannot exist, reported as a missing class, INSIDE the repository
that ships the file. Same shape as the gate-30 finding in #276: the path the
gate derives is not the path the app uses.
The false FAIL was only half of it. Where a DI binding rescued the absence the
loop `continue`d, so the method-existence check never ran — measured on two new
fixtures differing by exactly one renamed method: at origin/main BOTH report
PASS, including the one whose route resolves to nothing. That is #265's defect
at a different address.
Both candidate paths are probed and the one that EXISTS wins; when neither does
the lib/Controller/ spelling is reported exactly as before, so a genuinely
missing controller reads the same as it always has.
Also: test_gate_route_registration.sh hardcoded its runner, so pointing
HYDRA_GATES_RUNNER_UNDER_TEST at a pre-fix tree silently kept running the FIXED
one and reported all-green. A mutation check that cannot fail is the same defect
as a gate that cannot fail. With the override honoured, the suite is 5 failures
red against origin/main and green after.
And test-hydra-gates-bin.sh's 'with src/ present, every src-guarded gate really
runs' loop had a stale premise: #276 gave that fixture two real .vue files
(because gates 26/31/32 now distinguish 'src/ exists' from 'src/ contains what I
read'), so gates 12 and 13 correctly run and PASS there. They go back in the
loop, and the loop now asserts its own premise first. The zero-.vue case is
asserted where its premise actually holds — the nldesign-shaped fixture in
test_gate_empty_scope_never_passes.sh.
52 helper suites pass, 60 bin assertions pass.
rubenvdlinde
pushed a commit
that referenced
this pull request
Aug 9, 2026
The branch was rebased from fef032b onto 34370f6 (#276) after main moved, which rewrote its commits. `git cherry HEAD origin/<branch>` reports NO commit on the remote that is missing from this history — the remote tip's two commits are the pre-rebase spelling of the first two here — so this merge records the reconciliation and keeps the rebased tree, which is a strict superset.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
…to check, and gate-5 punted to a package it was standing in Three additions after the first commit, all measured against gate package 34370f6 and reported by the runner's own `[hydra-gates] gate package:` line. gate-10 DECLARED `na` OVER A REPO FULL OF ITS OWN SUBJECT MATTER ----------------------------------------------------------------- The surface was `src/` only. nldesign's `src/` holds exactly one file — `manifest.json` — and its entire hand-written frontend lives in `js/`. So gate-10 announced "this repo ships no frontend" over a repo whose `js/admin.js` does precisely what the gate exists to catch: var settingsEl = document.getElementById('nldesign-settings'); var tokenSets = JSON.parse(settingsEl.getAttribute('data-token-sets')); The doriath AdminRoot defect, two-step form, in an ADMIN settings script. Four real findings. `na` REMOVES A GATE FROM COVERAGE ACCOUNTING, so this did not merely under-report — it left the denominator. The surface is now `src/` + `js/` (Nextcloud's conventional shipped-script directory), minus `*.min.js`: a committed bundle is not authored code. Fleet total 0 -> 4, all four in nldesign, all four genuine. AND A SCOPE-BLINDNESS BUG IN MY OWN TWO-STEP RULE -------------------------------------------------- Widening the surface exposed it. The two-step rule matches by NAME across the whole file. In `js/admin.js`, line 1114 binds `var btn = document.getElementById('nldesign-save-btn')`, and that one line put EVERY `btn` in a 1700-line file into scope — including three `forEach(function (btn) {...})` callbacks whose `btn` is the clicked element, i.e. the component's own markup. Four false positives from one binding. Rather than implement JS scoping, an AMBIGUOUS NAME IS DROPPED: if an identifier is ever a function parameter, the file cannot tell the two apart and declines to guess. That can only under-report, which is the right direction for a rule whose false positives would otherwise bury the real finding. 7 raw hits -> 4, and the 4 that remain are the server-rendered reads. gate-5 CALLED A FILE ABSENT FROM INSIDE THE REPOSITORY CONTAINING IT --------------------------------------------------------------------- Nextcloud resolves a route name `A\B\C` against `OCA\<App>\A\B\C`, which PSR-4 maps to `lib/A/B/CController.php`. The resolver rooted every namespaced name at `lib/Controller/` — correct for `Settings\FileSettings`, wrong for `AppHost\Controller\GenericHealth`, whose file is `lib/AppHost/Controller/GenericHealthController.php`. Measured on openregister, the repository that SHIPS those classes. The derived path did not exist, `_apphost_serves` then matched the name, and both entries were filed as "served by the OpenRegister AppHost generic controller (ADR-040); its auth attribute lives in the openregister package and is NOT visible from this repository" inside openregister. The gate punted to another package while standing in it, so `AppHost\Controller\GenericHealth#index` and `GenericMetrics#index` had their auth posture judged by nobody. A derived path is a GUESS; both roots are tried and an existing one wins. Both now resolve, both are correctly annotated (`#[PublicPage]` + `#[NoCSRFRequired]`), and openregister's gate-5 count is unchanged at 10 — this surfaced two unjudged endpoints without inventing a finding. The AppHost CONSUMER control still classifies its four absent generics as ADR-040-unresolved. TWO ACCEPTANCE ARMS A PLANTED DEFECT CANNOT REACH -------------------------------------------------- A plant only fires when the gate RUNS, so two classes of defect are invisible to it. Both are now asserted: ARM 4 an ATTRIBUTE-ONLY change is a change. `_filter_preexisting` (gates 6, 7, 8 in this band) compares a method against the base ref and moves unchanged entries out of the verdict. Before #276 the comparison began at the `function NAME(` line, so ADDING `#[NoAdminRequired]` above an existing unguarded method left the body byte-identical and the finding was suppressed — the single edit that changes a method's auth posture was the one the filter could not see. Verified BOTH ways on one fixture: package cdfbd7a gives `[gate-7] no-admin-idor: PASS` with the finding in `.log.preexisting`; package 34370f6 gives FAIL. #276 fixed the shared helper; this keeps it fixed for gate-7, whose entire scope is decided by that attribute. ARM 5 a BROKEN INTERPRETER is `SKIPPED (wiring)`, never PASS. With python3 exiting 1, gates 2, 3, 10 and 11 all skip. gate-3 is the control: on the same fixture it FAILS a real stub `run()` when python3 works, so the skip is the gate losing a verdict it demonstrably had. TWO REPO SHAPES, NOT ONE ------------------------ gate-11's repair is proven on three: larpingapp (vue-router 3, `new VueRouter`), scholiq (vue-router 4 `createRouter`, AppHost-adopting) and nldesign (no router at all -> `na`, verified honest: no `createRouter`/`new VueRouter` anywhere in the repo). gate-5's resolver is proven on openregister (ships the generics) and an AppHost consumer (does not). Full-tree verdicts vs cdfbd7a: larpingapp and scholiq IDENTICAL; docudesk differs only by the intended gate-9 false-positive removal; nldesign gains gate-10 FAIL (4 real findings) and gate-11 `na` (previously a green over zero bytes).
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
… an empty scope, and gate-10 excused itself from four real findings (#275) * fix(gates): gate-11 was dead fleet-wide, and eight gates passed over an empty scope Every gate in the 1–11 band was put through one acceptance test: plant ONE textbook true positive of exactly what the gate exists to catch, in a real fleet repo, and require the gate to FAIL and NAME it; then remove the plant and require the prior verdict back. Measured against gate package cdfbd7a. gate-11 admin-router was DEAD ----------------------------- It read four hard-coded paths — src/router/index.{js,ts}, src/router.{js,ts}. ONE fleet app of fifteen (softwarecatalog) has a file at any of them; the other fourteen build their router in src/main.js. Proof it was dead rather than merely unexercised: the doriath c7c72e9 defect re-planted verbatim into larpingapp's real router — routes.push({ path: '/settings', component: AdminRoot }) — reported PASS. The identical line in src/router.js reported FAIL. The detection logic was fine; the gate never opened the file. Routers are now discovered (anything under src/ that constructs one), the four legacy paths are kept, and a repo with no router is `na`. The path rule moved into check_admin_router.py, which resolves the enclosing route object: a bare `path: '/settings'` grep would have flagged openconnector's ADR-079 hand-off — the remediation — as the defect. Fleet churn: 0 findings across 16 repos. EIGHT GATES REPORTED PASS OVER A SCOPE THEY NEVER OPENED -------------------------------------------------------- A README-only commit run with --scope-to-diff produced PASS from gates 1, 2, 3, 5, 8, 9, 10 and 11 — four of them authorization gates. Gates 4/6/7 already said NOT APPLICABLE for the identical situation, which is what made the other eight readable as a result rather than an absence. All eight now say `na` with a reason (`na`, not `structural`, per #268: an empty ADR-020 scope is not a gap the author can close). gate-9 semantic-auth flagged the ESCALATION BRANCH -------------------------------------------------- The `!isAdmin` block was searched for a denial token AT ANY DEPTH, conflating two opposite postures: if (!isAdmin) { return 403; } admin IS required -> true positive if (!isAdmin) { if (!owner) 403; } admin NOT required -> false positive In the second a non-admin OWNER proceeds. Live on docudesk SigningController::cancelRequest (its own docudesk#100 fix) and procest InspectionChecklistController::submitResult. The remedy gate-9 printed for both — remove @NoAdminRequired, or switch to #[AuthorizedAdminSetting] — would have made per-user endpoints admin-only and deleted the owner checks' reason to exist. Now only an UNCONDITIONAL denial counts. Fleet: 28 -> 26 findings; the two removed are exactly those, and larpingapp's genuine CharactersController::downloadPdf finding is preserved. gate-2 forbidden-patterns failed in BOTH directions (#184) ----------------------------------------------------------- Six raw-text greps. Invisible: `var_dump ($x)` (PHP allows whitespace before the argument list), `die;` (a LANGUAGE CONSTRUCT, not a function), and `exit` in every form — die's exact synonym, absent from the pattern list, so one name was banned and the other left open. Reported: a comment saying "never use var_dump( here", and the string literal "select dd(x)". Now judged over a comment- and string-masked copy. `: never` exempts exit/die — a TYPE PHP itself checks, deliberately not the @SuppressWarnings docblock sitting above the one fleet instance, because a docblock is the load-bearing prose #196 was about. Fleet churn: 1 -> 1. gate-8 unsafe-auth-resolver used HARD-CODED INDENTATION ------------------------------------------------------- Body extraction terminated on `/^ \}/`, the catch block on `/^ \}/`. On a tab-indented file neither matches, so "the body" ran to end of file and a resolver that correctly RETHROWS was reported as a fail-open because an unrelated cache method further down returned null from its own catch. The apparent detection of tab-indented fail-opens was the same over-capture by luck. Now brace-walked over a comment-masked copy; the procest ZgwService fail-closed shapes stay excluded by construction rather than by indentation. gate-10 initial-state knew only the single-line shape ----------------------------------------------------- `getElementById(...)...\.dataset` requires lookup and read on ONE line, so the TWO-STEP form — what the doriath line becomes after any refactor — plus querySelector and getAttribute('data-*') all reported PASS. Widened along that one axis only; NOT to a bare `.dataset`, which would flag every legitimate event.target.dataset in the fleet. Two measured exclusions, each a rule rather than a waiver: a dataset key the same file also WRITES is the component's own bookkeeping, and data-requesttoken is not IInitialState data (its accessor is getRequestToken(), so gate-10's remedy would not apply). Fleet churn: 0 -> 0. gate-7 no-admin-idor missed the FULLY-QUALIFIED attribute ---------------------------------------------------------- The look-back matched `#[NoAdminRequired` only. Under `#[\OCP\AppFramework\Http\Attribute\NoAdminRequired]` a textbook IDOR fell out of scope and reported PASS. No fleet file uses that spelling today, which is why it needed closing deliberately — a false negative on a security gate leaves no log. WHAT WAS VERIFIED AND LEFT ALONE -------------------------------- gate-3's #269 repair HOLDS in both directions: an inert `$unused = 1;` no longer closes it, and canonical fail-safe delegation no longer trips it. gate-5's #269 repair HOLDS: prose naming an attribute does not satisfy it, the `@auth admin-only <reason>` declaration does, and a too-short reason does not. gate-4 already distinguishes "no advisories" from "audit did not run". gate-6 detects a zero-caller authorization method and goes quiet on one caller. gate-7 is CORRECTLY correlated — .github#160's anti-correlation does not reproduce: a leak FAILs and all four correct-fix shapes PASS. docudesk EmlPreviewController::preview is a genuine IDOR (un-redacted EML by caller-supplied fileId, no ownership check). Regression tests land in run-helper-suites.sh's discovery path and are mutation-checked against WHOLE pre-fix files, not single-site reverts: the pre-fix runner fails 19 assertions of test_gate_1_11_empty_scope_is_na.sh (including the gate-11 dead-gate case), the pre-fix check_semantic_auth.py fails 3, the pre-fix check_no_admin_idor.py fails 1. Full-tree verdicts before/after are IDENTICAL on larpingapp and openregister, and differ on docudesk only by the intended gate-9 false positive. * test(gates): gate-5's empty-diff verdict is `na`, matching the suite's own comment test_gate_route_auth.sh asserted PASS for a package.json-only diff while its own comment said "a PASS here is scoping, not absence" — a fact PASS does not state and NOT APPLICABLE does. Reclassified per #268. 6b/6c remain the controls: the identical finding must still FAIL once the diff touches the controller or appinfo/routes.php. * fix(gates 5,10): a gate declared `na` over the frontend it was built to check, and gate-5 punted to a package it was standing in Three additions after the first commit, all measured against gate package 34370f6 and reported by the runner's own `[hydra-gates] gate package:` line. gate-10 DECLARED `na` OVER A REPO FULL OF ITS OWN SUBJECT MATTER ----------------------------------------------------------------- The surface was `src/` only. nldesign's `src/` holds exactly one file — `manifest.json` — and its entire hand-written frontend lives in `js/`. So gate-10 announced "this repo ships no frontend" over a repo whose `js/admin.js` does precisely what the gate exists to catch: var settingsEl = document.getElementById('nldesign-settings'); var tokenSets = JSON.parse(settingsEl.getAttribute('data-token-sets')); The doriath AdminRoot defect, two-step form, in an ADMIN settings script. Four real findings. `na` REMOVES A GATE FROM COVERAGE ACCOUNTING, so this did not merely under-report — it left the denominator. The surface is now `src/` + `js/` (Nextcloud's conventional shipped-script directory), minus `*.min.js`: a committed bundle is not authored code. Fleet total 0 -> 4, all four in nldesign, all four genuine. AND A SCOPE-BLINDNESS BUG IN MY OWN TWO-STEP RULE -------------------------------------------------- Widening the surface exposed it. The two-step rule matches by NAME across the whole file. In `js/admin.js`, line 1114 binds `var btn = document.getElementById('nldesign-save-btn')`, and that one line put EVERY `btn` in a 1700-line file into scope — including three `forEach(function (btn) {...})` callbacks whose `btn` is the clicked element, i.e. the component's own markup. Four false positives from one binding. Rather than implement JS scoping, an AMBIGUOUS NAME IS DROPPED: if an identifier is ever a function parameter, the file cannot tell the two apart and declines to guess. That can only under-report, which is the right direction for a rule whose false positives would otherwise bury the real finding. 7 raw hits -> 4, and the 4 that remain are the server-rendered reads. gate-5 CALLED A FILE ABSENT FROM INSIDE THE REPOSITORY CONTAINING IT --------------------------------------------------------------------- Nextcloud resolves a route name `A\B\C` against `OCA\<App>\A\B\C`, which PSR-4 maps to `lib/A/B/CController.php`. The resolver rooted every namespaced name at `lib/Controller/` — correct for `Settings\FileSettings`, wrong for `AppHost\Controller\GenericHealth`, whose file is `lib/AppHost/Controller/GenericHealthController.php`. Measured on openregister, the repository that SHIPS those classes. The derived path did not exist, `_apphost_serves` then matched the name, and both entries were filed as "served by the OpenRegister AppHost generic controller (ADR-040); its auth attribute lives in the openregister package and is NOT visible from this repository" inside openregister. The gate punted to another package while standing in it, so `AppHost\Controller\GenericHealth#index` and `GenericMetrics#index` had their auth posture judged by nobody. A derived path is a GUESS; both roots are tried and an existing one wins. Both now resolve, both are correctly annotated (`#[PublicPage]` + `#[NoCSRFRequired]`), and openregister's gate-5 count is unchanged at 10 — this surfaced two unjudged endpoints without inventing a finding. The AppHost CONSUMER control still classifies its four absent generics as ADR-040-unresolved. TWO ACCEPTANCE ARMS A PLANTED DEFECT CANNOT REACH -------------------------------------------------- A plant only fires when the gate RUNS, so two classes of defect are invisible to it. Both are now asserted: ARM 4 an ATTRIBUTE-ONLY change is a change. `_filter_preexisting` (gates 6, 7, 8 in this band) compares a method against the base ref and moves unchanged entries out of the verdict. Before #276 the comparison began at the `function NAME(` line, so ADDING `#[NoAdminRequired]` above an existing unguarded method left the body byte-identical and the finding was suppressed — the single edit that changes a method's auth posture was the one the filter could not see. Verified BOTH ways on one fixture: package cdfbd7a gives `[gate-7] no-admin-idor: PASS` with the finding in `.log.preexisting`; package 34370f6 gives FAIL. #276 fixed the shared helper; this keeps it fixed for gate-7, whose entire scope is decided by that attribute. ARM 5 a BROKEN INTERPRETER is `SKIPPED (wiring)`, never PASS. With python3 exiting 1, gates 2, 3, 10 and 11 all skip. gate-3 is the control: on the same fixture it FAILS a real stub `run()` when python3 works, so the skip is the gate losing a verdict it demonstrably had. TWO REPO SHAPES, NOT ONE ------------------------ gate-11's repair is proven on three: larpingapp (vue-router 3, `new VueRouter`), scholiq (vue-router 4 `createRouter`, AppHost-adopting) and nldesign (no router at all -> `na`, verified honest: no `createRouter`/`new VueRouter` anywhere in the repo). gate-5's resolver is proven on openregister (ships the generics) and an AppHost consumer (does not). Full-tree verdicts vs cdfbd7a: larpingapp and scholiq IDENTICAL; docudesk differs only by the intended gate-9 false-positive removal; nldesign gains gate-10 FAIL (4 real findings) and gate-11 `na` (previously a green over zero bytes). --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
…or, and gate-20 had never fired at all (#277) * fix(gates 12-22): four gates could not detect the defect they exist for, and gate-20 had never fired at all Every gate in the 12-22 band was given one textbook true positive in a real fleet repo and asked to catch it. Measured against package sha cdfbd7a, and re-measured on top of fef032b (#272), across openregister (207 .vue, 794 spec scenarios), pipelinq (202 .vue, type:"dashboard" pages) and shillinq (an ADR-040 AppHost adopter that keeps its own SettingsController). Seven fired, named the plant, and returned to their exact prior verdict when it was removed: 12, 13, 14 (both invariants), 15, 16, 18, 19, 21, 22. Four did not. 1. GATE-20 HAD NEVER FIRED. NOT RARELY - NEVER. Its search was `grep -nE "->${_pat//(/\\(}" "${_file}"`, and the expanded pattern `->findObjects\(` begins with `-`, so grep parses it as OPTIONS: `grep: invalid option -- '>'`, exit 2, no output. `2>/dev/null || true` discarded the message and the status, every file came back with zero hits, and the gate printed PASS. Planting `$this->objectService->findObjects(...)` in openregister's ActionsController: PASS. Repairing the grep ALONE is not the fix. The receiver test was "the FILE mentions ObjectService somewhere", and with the grep working that yields 14 findings on openregister and 5 on shillinq, ALL FALSE - `createFromArray()` is a real method on OpenRegister's *Mappers*. The receiver is now part of the pattern. Fleet measurement across the three repos afterwards: ONE finding, and it is real - shillinq's BookingNotificationController resolves OCA\OpenRegister\Service\ObjectService from the container inside its non-admin authorisation guard and calls findObject(), which does not exist on it (real surface: find/findAll/saveObject/createObject/updateObject/ deleteObject). That is a BadMethodCallException in an auth guard, shipped. 2. GATE-17 WAS WRONG ABOUT THE SAME API, IN THE OTHER DIRECTION. Four of the six names in OBJECT_SERVICE_CRUD - findObjects, createFromArray, updateFromArray, deleteFromId - do not exist on ObjectService; they are precisely what gate-20 flags as fabricated. findAll/createObject/ updateObject/deleteObject were absent. And `^\s*return\s+new\s+JSONResponse` sits in WRAPPER_NOISE_PATTERNS, which was tested BEFORE the ObjectService check - so the commonest pass-through spelling of all, `return new JSONResponse($this->objectService->findAll([]));`, was discarded as "response wrapping" with the call still inside it. It did not merely miss the shape: RESCUE_PATTERNS' `\$this->\w+Service->\w+\(` then matched it and returned False. The gate rescued its own subject. Blast radius of both changes across openregister/pipelinq/shillinq: 0 new findings. The CRUD-name filter and the `@spec exclude` escape hatch are untouched. 3. GATE-14 NEVER JUDGED THE TEN ROUTES AppHost SUPPLIES (#265, closed). #223 taught invariant 1 about `Routes::standard()`. Invariant 2 asks the opposite question and still read route names as literals out of the leaf's own appinfo/routes.php, where those ten names never appear. Deleting `SettingsController::update()` from shillinq - which keeps its own controller, as aliasControllerUnlessLeafDefinesIt explicitly allows - leaves PUT /api/settings resolving to nothing. That is not a 404: the router matches, ControllerMethodReflector reflects, the request dies 500. shillinq's own docblock on update() spells the hazard out. Gate-14's findings log came back EMPTY and the gate said PASS. 4. THREE GATES REPORTED PASS OVER A CRASHED CHECKER. With python3 replaced by a stub that always exits 1 - so not one file was inspected by any python-backed gate: gate-12 SKIPPED (wiring) gate-15 PASS gate-17 SKIPPED (wiring) gate-16 PASS gate-18 PASS gate-19 FAIL - "an unreported number of scenario(s)" 15/16/18 wrote `2>/dev/null || true` and counted lines in an empty log. 19 read exit 1 as EXIT_FAIL from a helper that never printed its own summary, producing a blocking verdict with a count nobody measured. The three helpers now print a terminal `# count=` marker and the runner requires it; gate-18's helper contract is "always exit 0", so any non-zero exit is a wiring skip; gate-19 treats "EXIT_FAIL with no FAIL - N summary line" as wiring. check_spec_coverage.py and check_dashboard_antipattern.py also stop returning their finding COUNT as an exit status (#209). 5. GATES 12 AND 13 PASSED OVER A src/ WITH ZERO .vue (#274). `[ -d src ]` is not `src/ contains a Vue component`. nldesign's src/ holds one manifest.json, and at fef032b both gates print PASS there - the shape that let twelve gates certify nldesign in #225, still current, not one `rm` away. Now `na`, with the judgement #274 asked for stated in the reason: NcSelect / NcModal / NcDialog are Vue SFC components, a PHP template cannot instantiate one, so these two stay .vue-only and gate-40 keeps the language-agnostic input-label rule for templates/. 6. GATE-22's VERDICT DEPENDED ON WHERE THE GATES WERE CHECKED OUT. `require('ajv/dist/2020')` resolves relative to check_manifest.js, never to the repo under test. Run from openregister's own root with node_modules/ajv PRESENT in that root, the validator printed "Ajv is not resolvable from this process (no node_modules, no NODE_PATH)" - false - and gate-22 went FAIL; exporting NODE_PATH to that same directory flipped it to PASS. Same tree, same package, two verdicts. Resolution is now anchored on the manifest's own repo root, then cwd, then this package, and the degradation names every directory it searched. 7. GATE-15's SLOT MATCHER HAD AN ATTRIBUTE-ORDER BLIND SPOT. `<template #widget-x class="wide">` and `<template #widget-x='{i}'>` did not match - the pattern assumed the slot binding was the last thing before `>`. Same class as gate-44's first-attribute stop in #272. TESTS test_gate_or_objectservice_surface.sh NEW - gate-20 and gate-17, each with the silent sibling that must stay silent (a mapper's real createFromArray; a domain-named method with an identical body), plus a direct assertion that a grep pattern starting with `-` is parsed as options test_gate_crashed_checker_is_not_a_finding.sh all six python-backed gates under a broken interpreter, generically, so a future gate inherits the assertion test_gate_empty_scope_never_passes.sh gates 12/13 on an nldesign-shaped repo, with the one-.vue control test_gate_route_registration.sh new fixture routes-standard-missing- update/, differing from its sibling by exactly the deleted method test_check_manifest.sh ajv resolved from the subject's node_modules with cwd and NODE_PATH pointing elsewhere test_check_dashboard_antipattern.py four slot-tag spellings that were silent misses Every one mutation-checked against the pre-fix tree: the crashed-checker arm is 7 failures red at cdfbd7a, the 12/13 arm 3 failures red at fef032b, the slot-tag arm 4 subtests red, and the ajv anchor red - all green after. 50 helper suites pass, 59 bin-level assertions pass. * fix(tests): the bin suite asserted that src/ existing means gates 12/13 ran test-hydra-gates-bin.sh looped 12 and 13 into 'with src/ present, every src-guarded gate really runs'. That fixture's src/ holds one .js file and no .vue, so the assertion encoded the belief this PR removes: that `[ -d src ]` is the same as 'the gate has a subject'. Both gates printed PASS there — an empty glob under an existing src/, nldesign's exact shape. They are now asserted separately and more strictly: not 'did not say na', but 'said na and NAMED the empty .vue glob'. PASS is called out by name as the regression it would be. 61 bin assertions pass, 50 helper suites pass. * fix(gates 12-22): four gates could not detect the defect they exist for, and gate-20 had never fired at all Every gate in the 12-22 band was given one textbook true positive in a real fleet repo and asked to catch it. Measured against package sha cdfbd7a, and re-measured on top of fef032b (#272), across openregister (207 .vue, 794 spec scenarios), pipelinq (202 .vue, type:"dashboard" pages) and shillinq (an ADR-040 AppHost adopter that keeps its own SettingsController). Seven fired, named the plant, and returned to their exact prior verdict when it was removed: 12, 13, 14 (both invariants), 15, 16, 18, 19, 21, 22. Four did not. 1. GATE-20 HAD NEVER FIRED. NOT RARELY - NEVER. Its search was `grep -nE "->${_pat//(/\\(}" "${_file}"`, and the expanded pattern `->findObjects\(` begins with `-`, so grep parses it as OPTIONS: `grep: invalid option -- '>'`, exit 2, no output. `2>/dev/null || true` discarded the message and the status, every file came back with zero hits, and the gate printed PASS. Planting `$this->objectService->findObjects(...)` in openregister's ActionsController: PASS. Repairing the grep ALONE is not the fix. The receiver test was "the FILE mentions ObjectService somewhere", and with the grep working that yields 14 findings on openregister and 5 on shillinq, ALL FALSE - `createFromArray()` is a real method on OpenRegister's *Mappers*. The receiver is now part of the pattern. Fleet measurement across the three repos afterwards: ONE finding, and it is real - shillinq's BookingNotificationController resolves OCA\OpenRegister\Service\ObjectService from the container inside its non-admin authorisation guard and calls findObject(), which does not exist on it (real surface: find/findAll/saveObject/createObject/updateObject/ deleteObject). That is a BadMethodCallException in an auth guard, shipped. 2. GATE-17 WAS WRONG ABOUT THE SAME API, IN THE OTHER DIRECTION. Four of the six names in OBJECT_SERVICE_CRUD - findObjects, createFromArray, updateFromArray, deleteFromId - do not exist on ObjectService; they are precisely what gate-20 flags as fabricated. findAll/createObject/ updateObject/deleteObject were absent. And `^\s*return\s+new\s+JSONResponse` sits in WRAPPER_NOISE_PATTERNS, which was tested BEFORE the ObjectService check - so the commonest pass-through spelling of all, `return new JSONResponse($this->objectService->findAll([]));`, was discarded as "response wrapping" with the call still inside it. It did not merely miss the shape: RESCUE_PATTERNS' `\$this->\w+Service->\w+\(` then matched it and returned False. The gate rescued its own subject. Blast radius of both changes across openregister/pipelinq/shillinq: 0 new findings. The CRUD-name filter and the `@spec exclude` escape hatch are untouched. 3. GATE-14 NEVER JUDGED THE TEN ROUTES AppHost SUPPLIES (#265, closed). #223 taught invariant 1 about `Routes::standard()`. Invariant 2 asks the opposite question and still read route names as literals out of the leaf's own appinfo/routes.php, where those ten names never appear. Deleting `SettingsController::update()` from shillinq - which keeps its own controller, as aliasControllerUnlessLeafDefinesIt explicitly allows - leaves PUT /api/settings resolving to nothing. That is not a 404: the router matches, ControllerMethodReflector reflects, the request dies 500. shillinq's own docblock on update() spells the hazard out. Gate-14's findings log came back EMPTY and the gate said PASS. 4. THREE GATES REPORTED PASS OVER A CRASHED CHECKER. With python3 replaced by a stub that always exits 1 - so not one file was inspected by any python-backed gate: gate-12 SKIPPED (wiring) gate-15 PASS gate-17 SKIPPED (wiring) gate-16 PASS gate-18 PASS gate-19 FAIL - "an unreported number of scenario(s)" 15/16/18 wrote `2>/dev/null || true` and counted lines in an empty log. 19 read exit 1 as EXIT_FAIL from a helper that never printed its own summary, producing a blocking verdict with a count nobody measured. The three helpers now print a terminal `# count=` marker and the runner requires it; gate-18's helper contract is "always exit 0", so any non-zero exit is a wiring skip; gate-19 treats "EXIT_FAIL with no FAIL - N summary line" as wiring. check_spec_coverage.py and check_dashboard_antipattern.py also stop returning their finding COUNT as an exit status (#209). 5. GATES 12 AND 13 PASSED OVER A src/ WITH ZERO .vue (#274). `[ -d src ]` is not `src/ contains a Vue component`. nldesign's src/ holds one manifest.json, and at fef032b both gates print PASS there - the shape that let twelve gates certify nldesign in #225, still current, not one `rm` away. Now `na`, with the judgement #274 asked for stated in the reason: NcSelect / NcModal / NcDialog are Vue SFC components, a PHP template cannot instantiate one, so these two stay .vue-only and gate-40 keeps the language-agnostic input-label rule for templates/. 6. GATE-22's VERDICT DEPENDED ON WHERE THE GATES WERE CHECKED OUT. `require('ajv/dist/2020')` resolves relative to check_manifest.js, never to the repo under test. Run from openregister's own root with node_modules/ajv PRESENT in that root, the validator printed "Ajv is not resolvable from this process (no node_modules, no NODE_PATH)" - false - and gate-22 went FAIL; exporting NODE_PATH to that same directory flipped it to PASS. Same tree, same package, two verdicts. Resolution is now anchored on the manifest's own repo root, then cwd, then this package, and the degradation names every directory it searched. 7. GATE-15's SLOT MATCHER HAD AN ATTRIBUTE-ORDER BLIND SPOT. `<template #widget-x class="wide">` and `<template #widget-x='{i}'>` did not match - the pattern assumed the slot binding was the last thing before `>`. Same class as gate-44's first-attribute stop in #272. TESTS test_gate_or_objectservice_surface.sh NEW - gate-20 and gate-17, each with the silent sibling that must stay silent (a mapper's real createFromArray; a domain-named method with an identical body), plus a direct assertion that a grep pattern starting with `-` is parsed as options test_gate_crashed_checker_is_not_a_finding.sh all six python-backed gates under a broken interpreter, generically, so a future gate inherits the assertion test_gate_empty_scope_never_passes.sh gates 12/13 on an nldesign-shaped repo, with the one-.vue control test_gate_route_registration.sh new fixture routes-standard-missing- update/, differing from its sibling by exactly the deleted method test_check_manifest.sh ajv resolved from the subject's node_modules with cwd and NODE_PATH pointing elsewhere test_check_dashboard_antipattern.py four slot-tag spellings that were silent misses Every one mutation-checked against the pre-fix tree: the crashed-checker arm is 7 failures red at cdfbd7a, the 12/13 arm 3 failures red at fef032b, the slot-tag arm 4 subtests red, and the ajv anchor red - all green after. 50 helper suites pass, 59 bin-level assertions pass. * fix(tests): the bin suite asserted that src/ existing means gates 12/13 ran test-hydra-gates-bin.sh looped 12 and 13 into 'with src/ present, every src-guarded gate really runs'. That fixture's src/ holds one .js file and no .vue, so the assertion encoded the belief this PR removes: that `[ -d src ]` is the same as 'the gate has a subject'. Both gates printed PASS there — an empty glob under an existing src/, nldesign's exact shape. They are now asserted separately and more strictly: not 'did not say na', but 'said na and NAMED the empty .vue glob'. PASS is called out by name as the regression it would be. 61 bin assertions pass, 50 helper suites pass. * fix(gate-14): a namespaced route name resolves to the app root, not to lib/Controller/ NC's RouteParser::buildControllerName() does not prefix the app namespace when the route name already contains a backslash, so `AppHost\Controller\GenericHealth#index` is looked up as the bare class `AppHost\Controller\GenericHealthController`. PSR-4 maps `OCA\<App>\` onto `lib/`, so that class lives at lib/AppHost/Controller/ — not under lib/Controller/, which is the only place the resolver looked. Reproduced against this package's own gates-23-33/planted fixture: lib/Controller/AppHost/Controller/GenericHealthController.php route='AppHost\Controller\GenericHealth#index' rule=controller-class-not-found a path that cannot exist, reported as a missing class, INSIDE the repository that ships the file. Same shape as the gate-30 finding in #276: the path the gate derives is not the path the app uses. The false FAIL was only half of it. Where a DI binding rescued the absence the loop `continue`d, so the method-existence check never ran — measured on two new fixtures differing by exactly one renamed method: at origin/main BOTH report PASS, including the one whose route resolves to nothing. That is #265's defect at a different address. Both candidate paths are probed and the one that EXISTS wins; when neither does the lib/Controller/ spelling is reported exactly as before, so a genuinely missing controller reads the same as it always has. Also: test_gate_route_registration.sh hardcoded its runner, so pointing HYDRA_GATES_RUNNER_UNDER_TEST at a pre-fix tree silently kept running the FIXED one and reported all-green. A mutation check that cannot fail is the same defect as a gate that cannot fail. With the override honoured, the suite is 5 failures red against origin/main and green after. And test-hydra-gates-bin.sh's 'with src/ present, every src-guarded gate really runs' loop had a stale premise: #276 gave that fixture two real .vue files (because gates 26/31/32 now distinguish 'src/ exists' from 'src/ contains what I read'), so gates 12 and 13 correctly run and PASS there. They go back in the loop, and the loop now asserts its own premise first. The zero-.vue case is asserted where its premise actually holds — the nldesign-shaped fixture in test_gate_empty_scope_never_passes.sh. 52 helper suites pass, 60 bin assertions pass. * fix(gate-16): deleting an @SPEC tag was the one edit the gate could not see `_overlaps` walks FORWARD from the declaration line through the method body. The docblock sits ABOVE the declaration, and the docblock is the only place `@spec` can live — so the scope window never contained the tag. Measured on a two-method fixture: delete the `@spec openspec/...` line, leave the body byte-identical, and the helper prints `# count=0` and exits 0. Every @SPEC tag in a repository can be stripped and gate-16 stays green. `run_gate` also skipped any file whose `added` set was empty, and a pure deletion produces exactly that, so the file was never even opened. Same family as filter_preexisting_methods.py filing an auth-attribute removal as pre-existing: a body-shaped scope cannot see a change that is not in the body. Gate-16 does NOT route through that helper — its four call sites are gates 6, 7, 8 and 30, and check_spec_coverage.py references it zero times — it arrives at the same blind spot by its own path. The fix keys on 'a tag was TAKEN AWAY', not on 'a docblock was touched'. A file that lost a tag is evaluated twice — as it is, and as it was at the base, with the same walkers and therefore the same exemptions — and only the DIFFERENCE is reported. So: delete a tag -> the method that lost it, and only it typo-fix a legacy untagged docblock -> clean (no inherited debt surfaced) add a new untagged method -> still caught, unchanged All three asserted, the second being the anti-widening control. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
The branch was rebased from cdfbd7a onto 48c88ba after four gate-package releases landed mid-session (#272, #275, #276, #280/#282). The rebase gave the same content a new history, which is a force-push, and force-push is blocked on shared branches for good reason. The tree here is IDENTICAL to the rebased HEAD — this commit only re-attaches the old tip as a second parent so the push is a fast-forward.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 13, 2026
…422) (#445) Two more #415-class false negatives, both in the RUNNER's own inline greps. Each was reproduced on origin/main before it was touched, through the gate, with a fixture whose only evidence is a comment. gate-14 route-reachability, invariant 1 ThingController::orphanReport(): JSONResponse, no route entry -> FAIL — rule=missing-route (correct) the same tree with routes.php gaining ONE LINE `// TODO: wire up 'thing#orphanReport' once the export lands.` -> PASS <- the defect gate-3 stub-scan, caller-identity arm public function authorize(string $uid, string $id): bool { … } with $uid never referenced -> FAIL — rule=caller-identity-ignored (correct) the same method with ONE LINE `// TODO: verify $uid actually owns this object before returning true.` -> PASS <- the defect gate-14's in-code note at that grep says comment hits are "vanishingly rare". It anticipated the false POSITIVE and missed the false NEGATIVE — the direction in which the endpoint 404s at runtime and the gate says the route exists. gate-3's arm exists BECAUSE the builder's fix-mode wrote methods that accept a caller identity and ignore it (decidesk#45); a stub that DOCUMENTS what it does not do was being reported finished. A THIRD one, found while writing the fixture and not in the survey: a commented-out `'resources' => [...]` block exempted a live controller's whole CRUD quintet from invariant 1. Written as a control, the revert says it FLIPS. MEASURED ON REAL REPOS, BOTH DIRECTIONS. Full runs over procest, opencatalogi, openregister, softwarecatalog, docudesk and larpingapp, before and after: EVERY COUNT IDENTICAL. procest g3=3 g14=0 docudesk g3=0 g14=12 opencatalogi g3=0 g14=0 larpingapp g3=0 g14=0 openregister g3=0 g14=0 softwarecatalog g3=0 g14=0 Agreement is what a dead rig looks like, so both gates were given a POSITIVE CONTROL on a real repository — a COPY of docudesk, the one repo with a non-zero gate-14 baseline, whose 12 genuine findings the copy reproduces: gate-14 one comment line added to appinfo/routes.php naming 'portalSigningReceiver#viewDocument' — one of its three real missing-route findings on a live controller origin/main 12 findings -> 11 (a real one hidden) fixed 12 -> 12 gate-3 a stub service method taking $uid and ignoring it origin/main FAIL — 1 fixed FAIL — 1 (both see it) + `// TODO: verify $uid actually owns this report…` origin/main PASS (blind) fixed FAIL — 1⚠️ The gate-3 control is reported in TWO steps on purpose. Step (a) — both arms FAIL — is not the measurement; it is what makes step (b) one. Read alone, "1 = 1" looks like "the change did nothing", which is exactly what an arm that never tested anything also looks like.⚠️ STRING CONTENTS SURVIVE IN BOTH, AND IN BOTH THEY ARE THE EVIDENCE. `php_mask` keeps literals by default and that is not an oversight here: gate-14 a route name IS the literal 'thing#index' and nothing else. Blanking literals would not widen this gate, it would delete every route in the file. gate-3 PHP INTERPOLATES `"no such user: $uid"` — that is a genuine use of the parameter. Blanking literals would report correct methods as unfinished stubs. The single-quoted '$uid' that is NOT a use stays a residual false negative and is #424's; it is the fail-safe direction for a gate that accuses a method of being unfinished. Arms 3 and 6 of the new suite go red the moment somebody generalises "strings are not evidence" across this file. A MASK THAT CANNOT BE PRODUCED IS NOT A LICENCE TO GRADE RAW TEXT. Both gates now decline — SKIPPED (wiring) — when source_scope.py is missing or fails its positive control, rather than falling back to the raw file. A silent fallback is precisely the false negative being closed and it leaves no log to notice; this package has found that shape four times (#147, #245, #276, #374). gate-3's guard fires on the ARM, not the file: without it the gate would print PASS with one of its four arms switched off. Arm 7 removes source_scope.py from a copy of the package and asserts both gates decline. The shared `_php_code_copy` helper carries its own positive control, once per run: of `// $uid` and `$uid`, exactly ONE must survive. A helper that echoed its input back would put every caller straight back into the false negative, which is gate-5's rule (#147, #245) applied to the two gates it had not reached. It is NOT a second copy of gate-5's `_ra_masked_copy` — folding gate-5 into it means re-proving gate-5's suite and belongs in its own change. Invariant 2 still reads routes.php RAW. Its exposure is the OPPOSITE direction (a commented-out route name manufacturing a phantom route) and belongs with the false-positive half, #423. Changing both here would put two independent verdict changes behind one measurement. 🔴 REPORTED, NOT FIXED: the runner's `_php_code_only` (~L919) is a line-PREFIX filter, so an unprefixed interior line of a `/* */` block survives it. Reproduced: a removal note reading "this app used to return \OCA\OpenRegister\AppHost\Routes::standard($extra) … It no longer does" sets _HYDRA_APPHOST_ROUTE_TABLE=1 and injects ten canonical route names, exempting all of them from invariant 1. The comment directly above that helper already records this class — it says the first cut was raw greps and that a fixture "was EXEMPTED BY ITS OWN EXPLANATION" — and it was then repaired with a filter that only handles the comment shape that fixture used. It gates exemptions read by gates 5, 14, 30 and 56, so it needs its own change and its own before/after. TESTS. New suite test_gate_3_14_comment_evidence.sh, 9 arms; discovered by tests/run-helper-suites.sh with no workflow edit. Reverted against origin/main — via a separate `git worktree` of origin/main, so SCRIPT_DIR resolves to that tree's own lib/ — three FLIP (arms 2, 4, 5) and arms 1, 3 and 6 pass either way and are labelled CONTROLS. Arm 7 is neither: on origin/main neither gate consults the helper at all, so it exists because the fix creates the dependency. Refs #422, #415. Sibling PRs cover the PHP call-site gates and the registry/config gates; gate-38 is BLOCKED on #424 (its fix needs markup masking, and routing it into source_scope.markup_mask today would close its comment half and leave the delimiter half armed). gate-30 and gate-1 are deliberately NOT in this sweep — see the findings note. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Acceptance test applied to every gate in the 23–33 band: plant ONE textbook true positive in a real fleet repo, run the gate, require it to FAIL and NAME the defect, remove the plant, require the prior verdict back, and keep a clean sibling passing so no repair is a widening.
[hydra-gates] gate package: cdfbd7ab1e28d8619fafa6cf772b0013ca4f9595.lib/Service/TenantIsolationService.phpregisterIntegrationid renamed away from itsLeafDescriptor#[NoAdminRequired]endpoint + route, no contract testsrc/views/*.vuewith no baseline->getLeaf('docudesk.…')@license MITvs composerEUPL-1.2/docs/added to.gitignoreover 633 tracked files#[PublicPage]deleted fromGenericHealthController::index<img>with noaltafter a nested<template #slot>na<div @click>after a nested slot, below an HTML commentnacriticalviolation / a{}report{}→ PASSThe seven repairs
gate-23 — the linter is bake-in gated: in WARN mode it exits 0 whether it matched nothing or everything, and the gate read only the byte. Separately, its
grep -v -i openregisterexclusion reads the file path, which inside the openregister repository never contains that string — 33 findings there, every one a canonical OpenRegister class told to consume itself, and the linter starts BLOCKING on 2026-08-13. Now: identity fromappinfo/info.xml<id>; the ADR-022 "consume the OR abstraction" rules do not apply to the repo that provides them (announced, never silent); the linter emitsor_abstraction_findings=<n>and the gate states the count and surfaces the matched rules even when it cannot block. openregister goes 35 matched paths → 1 (PDOK, whose provider is openconnector — still applies, still fires).gate-24 — openregister's wrapper prints
canonical JS check not found locally — skippingand exits 0. The gate read that as a PASS. A self-declared skip is now classified on the gate's own subject matter (nawith no leaves,structuralwith leaves).gate-26 —
check_visual_coverage.pyreturned the finding count as its exit status. Measured on openregister: 260 uncovered pages → exit 4 →FAIL — 4; 256 → exit 0 →[gate-26] visual-coverage: PASS, with the helper's own stdout readingFAIL — 256on the same run. This is #209 in a second gate. Status vocabulary now matches gate-25's; the count travels on stdout; stderr is kept; empty scope isna; and it no longer defaultsHYDRA_GATE_BASE_REF(#242), so full-repo mode reaches it — doriath has 10 uncovered page components CI has never seen.gate-27 —
2>/dev/null || truearound a helper whose only nonzero exit is a crash. Measured: 404 files in scope, none judged, verdict PASS. NowSKIPPED (wiring)with the traceback kept.gate-29 — a bare
[ fail -eq 0 ] && PASSreachable by both guards, so an unscoped run and a repo with no.gitignoreprinted the same PASS as a real inspection. Nowna, with the checked rule count stated on a genuine pass. The ignore prefix is also escaped before being used as an ERE. (The gate does not callgit check-ignoreat all, so the missing---no-indextrap does not apply — a comment now says so, for the next editor.)gate-30 —
AppHost\Controller\GenericHealth#indexmaps by PSR-4 tolib/Controller/AppHost/Controller/…, but openregister DI-binds it tolib/AppHost/Controller/GenericHealthController.php. The gate called the file "not present in this repository" — inside the repository that contains it — so/api/healthand/api/metricswere judged by this gate nowhere in the fleet. Resolved by basename underlib/when exactly one tracked file matches. The launchpad carve-outs are unchanged and asserted in the suite: a per-object GET and a POST are still not scrape targets, because the only remedy for flagging them is publishing an outbound-ping oracle.gate-33 — a
{}report parsed as "no violations" and reported PASS. A JSON file with noviolationskey is not an axe result object; it is now a wiring skip, and a real report prints how many violations a PASS is a pass over.Shared helper, affects gates 5/9/30 —
filter_preexisting_methods.pycompared method bodies starting at thefunction NAME(line, so deleting#[PublicPage]left the body byte-identical and the finding was moved into.preexisting. Measured: findings log empty,.preexistingholding the finding, gate-30 PASS. The comparison now includes the attribute/docblock region — a change that can only move entries back into the findings log.Tests
All mutation-checked against the pre-fix files (whole pre-fix package as the mutant):
scripts/lib/test_gates_23_33_never_green_over_nothing.sh— 39 assertions over aplanted/+clean/fixture pair plus a crashed-checker arm. 12 fail againstmain's checkers.scripts/lib/test_check_visual_coverage.py— 10 assertions including the 256-case whose low byte is zero. 6 fail againstmain's helper. (The helper shipped with no tests at all.)test_filter_preexisting_methods.py— attribute-removal case (fails onmain) plus its anti-widening sibling.tests/test-hydra-gates-bin.sh— its "src/ exists so every src-guarded gate runs" control now supplies real markup and a real page component.[ -d src ]was never gates 26/31/32's subject matter, and asserting PASS over asrc/holding one.jsfile is exactly the nldesign shape this work exists to stop.Full suites:
tests/run-helper-suites.sh49 passed / 0 failed (2 pre-existing quarantines),tests/test-hydra-gates-bin.sh59 passed / 0 failed.Not changed, deliberately
lib/Listener/AuditListener.php.nawhen nothing was opened", or "the provider of an abstraction is not a consumer of it".