fix(gate-14): stop flattening namespaced route names; understand a DI-bound fully-qualified controller - #217
Merged
Conversation
…lly-qualified controller
MEASURED, on opencatalogi origin/development (9e63d9f3). gate-14
route-reachability reported 6 findings, all false:
lib/Controller/OCAOpenCatalogiAppHostControllerGenericDashboardController.php
route='OCAOpenCatalogiAppHostControllerGenericDashboard#catchAll'
rule=controller-class-not-found
Two defects, stacked, the outer one hiding the inner.
1. `while IFS='#' read ctrl method` — WITHOUT `-r`, in BOTH route gates.
`read` performs backslash removal, so every namespaced route name arrived
FLATTENED. `OCA\OpenCatalogi\AppHost\Controller\GenericDashboard` became
`OCAOpenCatalogiAppHostControllerGenericDashboard`; openregister's 55
`Settings\…` routes became `SettingsLlmSettings` and friends. That is why
_ctrl_path_from_name's `Settings\Foo -> lib/Controller/Settings/
FooController.php` branch — documented since the day it was written — had
never once been reached from either gate.
2. Un-flattened, the opencatalogi names are fully-qualified classes under the
app's own namespace, and no exemption helper understood that shape.
`_apphost_serves` is keyed on the five short slugs Bootstrap aliases;
`_di_binds_controller` rebuilds `<app_ns>\Controller\…` from a FILE PATH.
The route is bound in lib/AppInfo/Application.php under the name verbatim
+ `Controller`, which is the only name NC will ever look up for it:
App::main() does `$container->get($controllerName)` FIRST, and for a name
containing `\Controller\` the QueryException branch throws
"App … is not enabled" rather than rewriting the name.
Fix:
* `read -r` in gate-5 and gate-14.
* `_ctrl_path_from_name` maps a name fully qualified under the app's own
namespace to its PSR-4 path (`OCA\<App>\` -> `lib/`), so a leaf that
genuinely ships such a class is opened and judged rather than
unresolvable.
* new `_di_binds_fq_controller`, sharing the needle matcher with
`_di_binds_controller` via `_app_php_binds_class`. Evidence required is
unchanged in strength: the exact escaped literal within a few lines of a
registerService/registerServiceAlias call in lib/AppInfo/Application.php.
NOT a wildcard and NOT a backslash skip. An UNBOUND fully-qualified route
still FAILS — that is the invariant, and test_gate_fq_route_names.sh asserts
both arms. Its positive control was verified by reverting this commit's
run-hydra-gates.sh change: the bound fixture then goes
`[gate-14] route-reachability: FAIL — 4` with `rule=controller-class-not-found`
on flattened names, i.e. the exact opencatalogi shape.
Measured effect, full-tree, gate verdicts otherwise byte-identical:
opencatalogi gate-14 FAIL — 6 -> PASS
hermiq gate-14 FAIL — 6 -> PASS
openregister gate-14 FAIL — 53 -> FAIL — 2 (see PR body)
…ot just the last Measured on openregister: the route `AppHost\Controller\GenericHealth` resolved to `lib/Controller/AppHost\Controller/GenericHealthController.php` — a path with a literal backslash inside it, which cannot exist, so the branch always answered "missing". Invisible until now because plain `read` deleted the backslashes before this branch could ever see two of them.
rubenvdlinde
pushed a commit
that referenced
this pull request
Aug 8, 2026
rubenvdlinde
added a commit
that referenced
this pull request
Aug 8, 2026
…ing gate that passed on nothing (#264) Closes #213. Closes #218. Closes #221. Closes #223. Closes #237. Two defects, five issues, three gates: (a) A shell/regex detail corrupted the input, so the gate measured something other than the code. gate-30's selector alternation is lowercase-only, so GenericMetrics/GenericHealth/chatHealth matched ZERO and it printed PASS over a 0-byte log; it also still carried the `read` without -r that #217 fixed in gates 5 and 14. (b) The gates modelled ONE registration idiom and flagged every other legitimate one: routes supplied by Routes::standard(), a Bootstrap::register() call moved into a registrar, a credential resolved one frame down, a controller whose name merely contains "health". gate-30 can no longer print PASS without having opened a monitoring method: every other outcome is a stated NOT APPLICABLE / SKIPPED with counts, and PASS states how many endpoints it inspected. 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.
What this fixes
gate-14 route-reachabilityreported 6 findings on opencatalogiorigin/development(9e63d9f3), all false:Two defects, stacked. The outer one hid the inner one.
1.
readwithout-r— every namespaced route name was flattenedBoth route gates read
appinfo/routes.phpthroughwhile IFS='#' read ctrl method.readperforms backslash removal, soOCA\OpenCatalogi\AppHost\Controller\GenericDashboardOCAOpenCatalogiAppHostControllerGenericDashboardSettings\LlmSettingsSettingsLlmSettingsConsequence:
_ctrl_path_from_name'sSettings\Foo -> lib/Controller/Settings/FooController.phpbranch — documented since the day it was written — had never once been reached from either gate. openregister's 55Settings\…routes all resolved to non-existent flattened paths.2. A fully-qualified route name was not a shape any helper understood
Un-flattened, opencatalogi's names are FQCNs under the app's own namespace, bound in
lib/AppInfo/Application.php:That binding is the only thing that can make the route work. From
/var/www/html/lib/private/AppFramework/App.phpin a live NC 33 container (docker exec procest-cilocal-nc awk '/public static function main/,/^\t}/' …):The literal lookup happens first, and for a name containing
\Controller\the fallback does not rewrite anything — it throws.RouteParser::buildControllerName()only doesunderScoreToCamelCase(ucfirst($controller)) . 'Controller', so the backslashes reach the container untouched.Neither existing helper could see this, each for its own correct reason:
_apphost_servesis keyed on the five short slugsBootstrap::register()aliases (dashboard). The route name is the whole class._di_binds_controllertakes a file path and rebuilds<app_ns>\Controller\…from it. Fed the flattened name it reconstructedOCA\OpenCatalogi\Controller\OCAOpenCatalogiAppHostControllerGenericDashboardController, which matches nothing.The change
read -rin gate-5 and gate-14._ctrl_path_from_namemaps a name fully qualified under the app's own namespace to its PSR-4 path (OCA\<App>\→lib/), so a leaf that genuinely ships such a class is opened and judged rather than being unresolvable. It also now turns every namespace separator into a path separator, not just the last —AppHost\Controller\GenericHealthused to producelib/Controller/AppHost\Controller/GenericHealthController.php, a path with a literal backslash that can never exist._di_binds_fq_controller, sharing the needle matcher with_di_binds_controllerthrough a new_app_php_binds_class. The_HYDRA_DI_NEEDLEenvironment technique is preserved verbatim (awk -vruns escape processing and silently destroys the\\pairs).What still FAILS — the invariant is intact
Not a wildcard, and not "a route name with backslashes is unverifiable, skip it" — that would retire reachability for every namespaced route in the fleet, silently. The evidence demanded is exactly what
_di_binds_controlleralready demands: the exact escaped literal within a few lines of aregisterService/registerServiceAliascall inlib/AppInfo/Application.php.An unbound fully-qualified route still fails, and the fixture proves it:
Also scoped to the app's own namespace on purpose:
OCA\SomeOtherApp\…is a class this repo does not own and cannot vouch for, so it is left to fail.Measured before / after — full-tree
opencatalogi
origin/development(9e63d9f3) — 64 gate verdicts captured on both arms and diffed; after normalising the per-run/tmp/hydra-gates.XXXXlog-dir token, exactly one line differs:gate-5staysPASSon both arms with the same 6 NOT-JUDGED entries; only their stated reason improves, from… is not present in this repository; controller class UNRESOLVEDtoserved by the OpenRegister AppHost generic controller (ADR-040). Runner exit code 12 → 11.Fleet, same A/B (
origin/mainworktree vs this branch):openregister's remaining 2 are a different, still-open false positive (see below).
gate-5on openregister goes 11 → 12 findings — both arms already FAIL, and the extra one is a true finding the gate was blind to because the name was flattened:Can-fail proof
hydra-gates/scripts/lib/test_gate_fq_route_names.sh(13 assertions, auto-discovered bytests/run-helper-suites.sh) with both arms:fq-bound→ gate-14 PASS,fq-unbound→ gate-14 FAIL.Stashing only the
run-hydra-gates.shchange and re-running the new suite — exit code read directly, not through a pipe:The positive-control arm goes red for the right reason:
rule=controller-class-not-foundon the bound fixture, on flattenedOCAFixture…names — the exact opencatalogi shape. With the change restored it is 13/13 green.Regression harness, all read directly:
Known remaining false positive — NOT fixed here
openregister names two routes with a relative namespace and binds them verbatim:
_di_binds_fq_controlleris deliberately scoped toOCA\<App>\…, so these two are still reported. They were reported before this PR too (as part of the 53), so this is not a regression — but it is the same class of defect one step over, and it wants a decision rather than a silent widening. Flagging rather than fixing.