Summary
gate-14 (route-reachability) invariant 1 decides "this controller method has no route" by grepping appinfo/routes.php for the literal string 'controller#method':
hydra-gates/scripts/run-hydra-gates.sh (at 756fe89), line 1539:
if ! grep -qF "'${_ctrl_slug}#${_m}'" appinfo/routes.php; then
echo "... rule=missing-route" >> "${_rr_log}"
An ADR-040 AppHost leaf does not write those literals. Its appinfo/routes.php returns \OCA\OpenRegister\AppHost\Routes::standard([...]), and the engine supplies the canonical set. So every canonical route reads as unrouted — a 404 claim about endpoints that are demonstrably served.
The gate already has the machinery for the other direction: _HYDRA_APPHOST / _apphost_serves / _di_binds_controller (added in #199) cover invariant 2, where a route names a controller file that is absent because AppHost aliases it. Invariant 1 has no equivalent. It fires precisely when the leaf does ship the controller (so aliasControllerUnlessLeafDefinesIt keeps the leaf's own class) and lets the engine register the route.
Measured on ConductionNL/openbuild @ c70f7b8d9
Full-tree run, no diff scoping:
[gate-14] route-reachability: FAIL — 7 unrouted method(s) or wrong-target route(s)
lib/Controller/DashboardController.php method=catchAll expected_route='dashboard#catchAll'
lib/Controller/DashboardController.php method=page expected_route='dashboard#page'
lib/Controller/PreferencesController.php method=getPreference expected_route='preferences#getPreference'
lib/Controller/PreferencesController.php method=setPreference expected_route='preferences#setPreference'
lib/Controller/SettingsController.php method=create expected_route='settings#create'
lib/Controller/SettingsController.php method=index expected_route='settings#index'
lib/Controller/SettingsController.php method=load expected_route='settings#load'
All 7 of 7 are the engine-supplied canonical set. Call path:
openbuild/appinfo/routes.php — the whole file is return \OCA\OpenRegister\AppHost\Routes::standard([ ...domain routes... ]);
openregister/lib/AppHost/Routes.php emits, verbatim:
['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'],
['name' => 'settings#index', 'url' => '/api/settings', 'verb' => 'GET'],
['name' => 'settings#create', 'url' => '/api/settings', 'verb' => 'POST'],
['name' => 'settings#update', 'url' => '/api/settings', 'verb' => 'PUT'],
['name' => 'settings#load', 'url' => '/api/settings/load', 'verb' => 'POST'],
['name' => 'preferences#getPreference', 'url' => '/api/preferences/{key}', 'verb' => 'GET'],
['name' => 'preferences#setPreference', 'url' => '/api/preferences/{key}', 'verb' => 'PUT'],
['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'],
['name' => 'health#index', 'url' => '/api/health', 'verb' => 'GET'],
plus the SPA catch-all (dashboard#catchAll) appended last. Every one of the 7 findings is in that list. openbuild/appinfo/routes.php says so in a comment at the top, naming the exact set.
_HYDRA_APPHOST is already 1 for this repo — lib/AppInfo/Application.php both references OCA\OpenRegister\AppHost\Bootstrap and calls Bootstrap::register(. Invariant 1 simply never consults it.
Positive control
The gate is not blind — it is reading a real absence and drawing the wrong conclusion from it. grep -F "'dashboard#page'" appinfo/routes.php genuinely returns nothing, because the literal is in openregister. The finding is accurate about the file and wrong about the route table.
Why this matters beyond noise
gate-14 is in the authorisation tier. Seven standing false positives in that tier is the "cries wolf on correct code" shape #153 already fixed for gate-5 — it trains readers to skip the whole tier. It also cannot be silenced safely per-repo: adding the literals to appinfo/routes.php would register duplicate route names, and Routes::standard() throws on duplicates by design.
Suggested fix
Mirror what invariant 2 already does. When appinfo/routes.php calls \OCA\OpenRegister\AppHost\Routes::standard(, treat the canonical name list as routed for invariant 1 — as an explicit list (dashboard#page, dashboard#catchAll, settings#index|create|update|load, preferences#getPreference|setPreference, metrics#index, health#index), sourced from openregister/lib/AppHost/Routes.php, never a wildcard. A wildcard would let any genuinely unrouted method hide behind AppHost adoption, which is the invariant worth keeping.
Evidence that the narrow list is sufficient here: settings#update is in the canonical list and is not among openbuild's findings, because openbuild's own SettingsController does not define update(). The list-based exemption therefore does not need to suppress anything the leaf actually owns.
Summary
gate-14 (route-reachability) invariant 1 decides "this controller method has no route" by grepping
appinfo/routes.phpfor the literal string'controller#method':hydra-gates/scripts/run-hydra-gates.sh(at756fe89), line 1539:An ADR-040 AppHost leaf does not write those literals. Its
appinfo/routes.phpreturns\OCA\OpenRegister\AppHost\Routes::standard([...]), and the engine supplies the canonical set. So every canonical route reads as unrouted — a 404 claim about endpoints that are demonstrably served.The gate already has the machinery for the other direction:
_HYDRA_APPHOST/_apphost_serves/_di_binds_controller(added in #199) cover invariant 2, where a route names a controller file that is absent because AppHost aliases it. Invariant 1 has no equivalent. It fires precisely when the leaf does ship the controller (soaliasControllerUnlessLeafDefinesItkeeps the leaf's own class) and lets the engine register the route.Measured on ConductionNL/openbuild @ c70f7b8d9
Full-tree run, no diff scoping:
All 7 of 7 are the engine-supplied canonical set. Call path:
openbuild/appinfo/routes.php— the whole file isreturn \OCA\OpenRegister\AppHost\Routes::standard([ ...domain routes... ]);openregister/lib/AppHost/Routes.phpemits, verbatim:plus the SPA catch-all (
dashboard#catchAll) appended last. Every one of the 7 findings is in that list.openbuild/appinfo/routes.phpsays so in a comment at the top, naming the exact set._HYDRA_APPHOSTis already 1 for this repo —lib/AppInfo/Application.phpboth referencesOCA\OpenRegister\AppHost\Bootstrapand callsBootstrap::register(. Invariant 1 simply never consults it.Positive control
The gate is not blind — it is reading a real absence and drawing the wrong conclusion from it.
grep -F "'dashboard#page'" appinfo/routes.phpgenuinely returns nothing, because the literal is in openregister. The finding is accurate about the file and wrong about the route table.Why this matters beyond noise
gate-14 is in the authorisation tier. Seven standing false positives in that tier is the "cries wolf on correct code" shape #153 already fixed for gate-5 — it trains readers to skip the whole tier. It also cannot be silenced safely per-repo: adding the literals to
appinfo/routes.phpwould register duplicate route names, andRoutes::standard()throws on duplicates by design.Suggested fix
Mirror what invariant 2 already does. When
appinfo/routes.phpcalls\OCA\OpenRegister\AppHost\Routes::standard(, treat the canonical name list as routed for invariant 1 — as an explicit list (dashboard#page,dashboard#catchAll,settings#index|create|update|load,preferences#getPreference|setPreference,metrics#index,health#index), sourced fromopenregister/lib/AppHost/Routes.php, never a wildcard. A wildcard would let any genuinely unrouted method hide behind AppHost adoption, which is the invariant worth keeping.Evidence that the narrow list is sufficient here:
settings#updateis in the canonical list and is not among openbuild's findings, because openbuild's ownSettingsControllerdoes not defineupdate(). The list-based exemption therefore does not need to suppress anything the leaf actually owns.