Skip to content

fix(gates 12-22): four gates could not detect the defect they exist for, and gate-20 had never fired at all - #277

Merged
rubenvdlinde merged 8 commits into
mainfrom
fix/gates-12-22-planted-defect-audit
Aug 9, 2026
Merged

fix(gates 12-22): four gates could not detect the defect they exist for, and gate-20 had never fired at all#277
rubenvdlinde merged 8 commits into
mainfrom
fix/gates-12-22-planted-defect-audit

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

What was measured

Every gate in the 12–22 band was given one textbook true positive of exactly what it exists to catch, in a real fleet repo whose shape suits it; then the plant was removed and the gate had to return to its exact prior verdict, and a clean repo had to stay clean. Three further arms were added after the earlier bands showed a plant cannot see them: break the interpreter, run against a repo whose subject the gate cannot see, and read the exit byte on a many-finding run.

Final measurement at [hydra-gates] gate package: ff8869d17ed38fbc52b746f99c6ca89abaf841d0 (this branch, merged with 48c88ba). Earlier arms measured at cdfbd7a, fef032b and 34370f6; each defect below is quoted with the sha it was measured at.

Repo shapes — two per gate where behaviour can differ by shape:

Repo Shape Why it matters here
openregister hand-declared appinfo/routes.php, 207 .vue, 794 spec scenarios, register JSONs the conventional shape
shillinq ADR-040 AppHost adopter — Routes::standard(), keeps its own SettingsController decisive for gate-14/#265
pipelinq hand-declared routes, six type:"dashboard" pages gate-15's subject
nldesign-shaped fixture src/ holding one manifest.json gates 12/13 zero-.vue
gates-23-33/planted (this package) DI-bound AppHost\Controller\GenericHealth gate-14 path derivation
Gate Planted true positive Fired? Repaired
12 nc-input-labels <NcSelect :reduce="(o) => o.value" />, no input-label na not PASS on zero-.vue src/
13 modal-isolation inline <NcModal> in src/views/ ➕ same
14 route-reachability (a) unrouted method (b) route→missing method (c) settings#update deleted from an AppHost adopter (d) DI-bound AppHost\Controller\GenericHealth ✅ ✅ #265 closed + PSR-4 path
15 dashboard-antipattern <CnDashboardPage> in a #widget-<id> slot ✅ crashed checker; attribute-order
16 spec-coverage changed method with no @spec; deleting an existing @spec ✅ crashed checker; tag-deletion
17 redundant-controller fetchAll() { return new JSONResponse($this->objectService->findAll([])); } ❌ PASS ✅ two causes
18 notification-dialect legacy dialect in a register JSON ✅ (7 tokens) ✅ crashed checker
19 e2e-coverage new #### Scenario: with no @e2e ✅ crashed checker
20 or-objectservice-api $this->objectService->findObjects(...) ❌ — had never fired anywhere, ever
21 conflict-markers real markers in lib/*.php ✅ — and not on a .md documenting them
22 manifest-validation invalid page type + additional property ✅ verdict depended on checkout layout

Seven defects

1. gate-20 had never fired. Not rarely — never. Its pattern expands to ->findObjects\(, which begins with -, so grep parses it as options (grep: invalid option -- '>', exit 2). 2>/dev/null || true discarded the message and the status, so every file returned zero hits and the gate printed PASS fleet-wide.

Repairing the grep alone is not the fix: the receiver test was "the file mentions ObjectService somewhere", which with a working grep yields 14 false findings on openregister and 5 on shillinqcreateFromArray() is a real method on OpenRegister's Mappers. The receiver is now part of the pattern. Measurement across all 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. A BadMethodCallException in an auth guard, shipped.

2. gate-17 was wrong about the same API, in the other direction. Four of six names in OBJECT_SERVICE_CRUD do not exist on ObjectService — they are what gate-20 flags as fabricated — and findAll/createObject/updateObject/deleteObject were absent. Separately, ^\s*return\s+new\s+JSONResponse sits in WRAPPER_NOISE_PATTERNS and was tested before the ObjectService check, so the commonest pass-through spelling was discarded as "response wrapping" with the call still inside it — then matched by RESCUE_PATTERNS and returned False. The gate rescued its own subject. Blast radius of both fixes across the three repos: 0 new findings.

3. gate-14 never judged the ten routes AppHost supplies (#265). #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 routes.php, where those ten never appear. Deleting SettingsController::update() from shillinq leaves PUT /api/settings resolving to nothing — not a 404: the router matches, ControllerMethodReflector reflects, the request dies 500. shillinq's own docblock spells the hazard out. Gate-14's findings log came back empty.

4. gate-14 derived a path the app does not use. AppHost\Controller\GenericHealth#index resolved to lib/Controller/AppHost/Controller/… while PSR-4 puts it at lib/AppHost/Controller/…, so the gate reported controller-class-not-found inside the repository that ships the file. And where a DI binding rescued the absence the loop continued — so the method-existence check never ran: two fixtures differing by one renamed method both reported PASS. #275 found the same root independently from gate-5's side and landed the two-root probe; this branch's resolver is now byte-identical to main's, and adds the gate-14 invariant-2 fixtures that assert it from the other direction.

5. Three gates reported PASS over a crashed checker. With python3 exiting 1 — nothing 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 summary — a blocking verdict carrying a count nobody measured.

6. gates 12 and 13 passed over a src/ with zero .vue (#274). [ -d src ] is not "src/ contains a Vue component". At fef032b both printed PASS on the nldesign shape. Now na, with the judgement #274 asked for written down: 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 rule for templates/. (#280 closed the gate-45 half; this is the gate-12 half.)

7. gate-16 could not see the deletion of its own tag. _overlaps walks forward from the declaration; the docblock is above it, and the docblock is the only place @spec lives. Delete the tag, leave the body byte-identical → # count=0, exit 0. Every @spec tag in a repository could be stripped and gate-16 stayed green. run_gate also skipped any file with an empty added set, which a pure deletion produces, so the file was never opened. Same family as filter_preexisting_methods.py filing an auth-attribute removal as pre-existing — gate-16 does not route through that helper (its four call sites are gates 6/7/8/30, and check_spec_coverage.py references it zero times); it reached the same blind spot by its own path.

Plus gate-22's verdict depended on where the gates were checked out (ajv resolved relative to check_manifest.js, never to the repo under test — run from openregister's root with node_modules/ajv present there it printed "not resolvable … no node_modules", which was false), and gate-15's slot matcher had gate-44's attribute-order blind spot (<template #widget-x class="wide"> did not match).

Exit is a status, count on stdout — swept, empirically

Helper Findings Exit
check_e2e_coverage.py 794 1
check_spec_coverage.py 239 1
detect-redundant-controllers.py 1 + # count= marker 1
check_dashboard_antipattern.py 0 + marker 0
check_manifest.js failed already clamped to 0/1 0/1/2/3

check_spec_coverage.py and check_dashboard_antipattern.py used to return the count; both are now a status plus a terminal # count= marker, which is also what lets the runner tell "clean" from "did not execute".

Anti-widening

  • gate-20: a mapper's real createFromArray() in a file mentioning ObjectService → not flagged.
  • gate-17: a domain-named method with a byte-identical body → not flagged.
  • gate-14: the other nine canonical AppHost names still resolve; the conventional ping#index sibling is unaffected; routes-standard/ still yields exactly one finding.
  • gate-16: a typo fix in a legacy untagged method's docblock → clean, no inherited debt surfaced.
  • gates 12/13: add one .vue carrying the defect and both go straight back to FAIL.
  • gate-18: a working python3 on the same fixture still FAILs the planted dialect.
  • gate-21: a .md documenting conflict markers is not flagged.
  • pipelinq and openregister, clean, whole repo: verdicts unchanged before and after across all eleven gates.

Tests — all mutation-checked against the pre-fix tree

Suite Red before
test_gate_or_objectservice_surface.sh (new) gate-20 + gate-17, each with its silent sibling, plus a direct assertion that a --leading grep pattern is parsed as options
test_gate_crashed_checker_is_not_a_finding.sh 7 failures — all six python-backed gates under a broken interpreter, asserted generically so a future gate inherits it
test_gate_empty_scope_never_passes.sh 3 failures — gates 12/13 on the nldesign shape, with the one-.vue control
test_gate_route_registration.sh 5 failuresroutes-standard-missing-update/ (#265) and psr4-namespaced-controller{,-missing-method}/. ⚠️ This suite hardcoded its runner, so HYDRA_GATES_RUNNER_UNDER_TEST silently kept running the fixed one and reported all-green — a mutation check that could not fail. Fixed here too.
test_check_spec_coverage.py tag-deletion caught; unrelated docblock edit clean
test_check_manifest.sh ajv anchor red before
test_check_dashboard_antipattern.py 4 subtests — slot-tag spellings that were silent misses

59 helper suites pass (2 quarantined, unchanged), 61 bin-level assertions pass.

Verdicts will move

gate-20 and gate-17 can now produce findings in repos that were green — starting with the shillinq auth-guard call above. Gates 12/13/15/16/18/19 can now report SKIPPED (wiring) or NOT APPLICABLE; neither is a pass.

Closes #265. Closes the gate-12 half of #274.

rubenvdlinde and others added 8 commits August 9, 2026 00:06
…or, 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.
…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.
…or, 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.
…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.
…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.
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.
…ot 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.
…ed-defect-audit

# Conflicts:
#	hydra-gates/scripts/run-hydra-gates.sh
#	hydra-gates/tests/test-hydra-gates-bin.sh
@rubenvdlinde
rubenvdlinde merged commit 566ac8c into main Aug 9, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant