fix(hydra-gates): two legitimate patterns the gates were blind to (planix) - #545
Conversation
…p declares no pages[] gate-30/gate-53's deepLink check resolved urlTemplates against pages[].route and nothing else. That is the whole route inventory for a manifest-driven app and EMPTY for one that renders a hand-written SPA, so the check was unsatisfiable for that class: planix declares five valid deepLinks against a vue-router table and got five FAILs, and the only way to pass was to declare pages the runtime would then try to render — breaking the app to please the gate. The routes are still declared, just in src/router/. discoverRouterRoutes() reads that table (shallow, string-literal only — a dynamically built route is simply not discovered) and it is consulted only when pages[] is empty, so manifest-driven apps are byte-identical. With no inventory of any kind the finding degrades to WARN: absence of evidence is not evidence of a broken link. Fixtures router-routes/ and router-routes-broken/ share one router table, so the pass/fail difference is the deepLink target alone — the new assertions cannot be satisfied by the check silently doing nothing.
…strap::register() The gate-14 AppHost detector required `AppHost\\Bootstrap` AND `Bootstrap::register(` in one file. That is one of two legitimate spellings. planix wires the same generics itself, because the one-call helper ALSO runs registerServices(), which aliases the leaf's Service\\SettingsService to the engine's AppHostSettingsService — an app shipping its own SettingsService then hands its own SettingsController the wrong class and dies with a TypeError on the first request (reproduced while attempting the 'canonical' refactor: planix returned 500 on every settings call until it was reverted). Registering the controllers and NOT the services is the only shape that works there. The detector now also accepts a file that names an AppHost Generic*Controller AND hands it to registerService(), in the same file, in non-comment code. The generic FQCNs are a closed set, so this cannot become a blanket exemption: an app must name the generic it aliases, and the slug list still bounds which absences are excusable. Fixture apphost-hand-rolled/ asserts both directions at once — the four AppHost slugs are served, and gadget#run (not an AppHost slug, controller genuinely absent) is STILL raised. 47/47 control pairs pass.
Second commit: gate-14 AppHost detectorSame failure class, same reporter. The AppHost detector required planix wires the same generics itself, and it must: I reproduced this rather than reasoned about it. Attempting the "canonical" one-call refactor on planix produced, on every settings request: The refactor was reverted; the app is healthy again. Registering the controllers and not the services is the only shape that works there — it is adoption, spelled the long way. EvidenceNew fixture
Only gate-65 (coding-standard) still fails on planix, and that is cleared by ConductionNL/planninq#333. |
`for db in keycloak` is a one-word loop: ShellCheck is right about the shape and wrong about the intent — the loop exists so the next ExApp's database is one edit. An explicit EXAPP_DATABASES array says that, and quoting $db fixes the word-splitting the original also had. Pre-existing; it fails the ShellCheck workflow on every PR to this repo because the wrapper exits 1 on any finding, including notes.
The gap
gate-30/gate-53's deepLink correspondence check resolved
urlTemplates againstpages[].routeand nothing else. That is the complete route inventory for a manifest-driven app — and empty for an app that renders a hand-written SPA.planix was the first app to trip it: five perfectly valid deepLinks, five FAILs, and the only way to pass would have been to declare
pages[]the manifest runtime then tries to render — i.e. break the app to please the gate. Its manifest says so in as many words: "planix renders no manifest-driven UI, so there is deliberately no menu/pages".Same family as the pageTemplates gap (#542) and the AppHost-detector gap: the first user of a legitimate pattern finds the checker's blind spot.
The fix
discoverRouterRoutes()reads the app'ssrc/router/table, consulted only whenpages[]is empty — manifest-driven apps take the identical path they did before. Parsing is deliberately shallow (apath:string literal in a file that looks like a router); anything cleverer would be a JS evaluator. A dynamically-built route is simply not discovered, which lands on the WARN path rather than a false FAIL.With no inventory of any kind (no pages, no parseable router) the finding degrades to WARN: absence of evidence is not evidence of a broken link.
Evidence — the check can still fail
Both new fixtures carry the same router table, so the pass/fail difference is the deepLink target alone; the assertions cannot be satisfied by the check silently doing nothing.
router-routes/— deepLinks match router routesrouter-routes-broken/— one deepLink (/invoices/{uuid}) the router does not declare/deepLinks/1, message namessrc/routernode scripts/lib/test_check_manifest_crossref.js— all assertions PASSED, including the four added here.🤖 Generated with Claude Code