Skip to content

fix(hydra-gates): two legitimate patterns the gates were blind to (planix) - #545

Merged
rubenvdlinde merged 3 commits into
mainfrom
fix/gates-deeplink-router-routes
Aug 21, 2026
Merged

fix(hydra-gates): two legitimate patterns the gates were blind to (planix)#545
rubenvdlinde merged 3 commits into
mainfrom
fix/gates-deeplink-router-routes

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

The gap

gate-30/gate-53's deepLink correspondence check resolved urlTemplates against pages[].route and 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's src/router/ table, consulted only when pages[] is empty — manifest-driven apps take the identical path they did before. Parsing is deliberately shallow (a path: 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.

control result
router-routes/ — deepLinks match router routes passes, 0 deeplink-route findings
router-routes-broken/ — one deepLink (/invoices/{uuid}) the router does not declare fails, exactly 1 error at /deepLinks/1, message names src/router
no pages[] and no parseable router WARN, exit 0
regression: manifest-driven app (hrmq, 109 pages) unchanged — same findings as before
planix (the reporter) 5 FAILs → 0

node scripts/lib/test_check_manifest_crossref.jsall assertions PASSED, including the four added here.

🤖 Generated with Claude Code

Conduction Release Bot added 2 commits August 22, 2026 00:06
…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.
@rubenvdlinde rubenvdlinde changed the title fix(hydra-gates): deepLink correspondence reads src/router when an app declares no pages[] fix(hydra-gates): two legitimate patterns the gates were blind to (planix) Aug 21, 2026
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Second commit: gate-14 AppHost detector

Same failure class, same reporter. The AppHost detector required AppHost\Bootstrap and Bootstrap::register( in one file — one of two legitimate spellings of ADR-040 adoption.

planix wires the same generics itself, and it must: Bootstrap::register() also runs registerServices(), which aliases the leaf's Service\SettingsService to the engine's AppHostSettingsService. An app that ships its own SettingsService then hands its own SettingsController the engine class.

I reproduced this rather than reasoned about it. Attempting the "canonical" one-call refactor on planix produced, on every settings request:

TypeError: OCA\Planix\Controller\SettingsController::__construct():
Argument #2 ($settingsService) must be of type OCA\Planix\Service\SettingsService,
OCA\OpenRegister\AppHost\Service\AppHostSettingsService given

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.

Evidence

New fixture apphost-hand-rolled/ asserts both directions from one fixture, so the exemption cannot widen into "this app registers services, therefore absences are fine":

assertion result
four AppHost slugs (dashboard/health/metrics/preferences) recognised as served not reported
gadget#run — not an AppHost slug, controller genuinely absent still FAILS
full gate-5/gate-14 control-pair suite 47/47 pass (was 41/41 before these fixtures)
planix, real repo gate-14 FAIL(6) → PASS; gate-53 FAIL(5) → PASS

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.
@rubenvdlinde
rubenvdlinde merged commit 55e794f into main Aug 21, 2026
37 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/gates-deeplink-router-routes branch August 21, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant