Three apps hit this in one week, each with a different and expensive consequence, and no existing gate can see it.
The defect
An app declares a schema in a register.d/*.json fragment. The register's own components.registers.<app>.schemas[] list does not name it. ImportHandler builds the register's schema ids by walking the declared list, so the schema is created and linked to nothing. Every read scoped to that register then misses it, and the failure is silent at every layer.
The three instances
decidiq: 106 declared, 95 attached, 11 never attached, including decision-template, approval-route and approval-action. Consequence: typeOf() set the schema, a register-scoped miss threw by design, a catch (\Throwable) logged it and returned null, and both the publish guard and the clause stamp were skipped. Every decision published with no legal remedy clause and answered 200 OK. Fixed in ConductionNL/decidiq#1353.
portaliq: 7 never attached (portalMandate, portalInvitation, portalReferenceLink, portalAccessRequest, portalFormBinding, portalIntakeSubmission, changeProposal). Their object routes 404. Compounded because info.version was frozen at 0.23.0, so no upgrade would ever have re-imported them. Fixed in ConductionNL/portaliq#622.
integriq: digitalPostMessage declared in components.schemas and in neither the register nor SCHEMA_SLUGS, while the service writes the record of what was sent to that slug inside a catch (Throwable) that only logs. Every record of what was posted as digital post was dropped, silently, on every send. Fixed in ConductionNL/integriq#2082.
Why nothing catches it
- gate-101 cannot see it: its scope is built from definition keys while a modular register lists slugs, so fragment-declared schemas are invisible to it. On decidiq it reported "checked 38 schema(s), 0 fail" while eleven were attached to nothing.
- Unit suites fake the object service, so they never exercise binding.
- An e2e that queries
/api/schemas sees the global list, where the schema exists. Only a register-scoped query reveals it.
- integriq happened to have a guard (
RegisterDescriptorTest::testRegisterDeclaresAllSchemaSlugs) and that is the only reason its instance was caught quickly. decidiq and portaliq had none.
Proposed gate
Replay the app's own fragment merge, then assert every declared schema appears in components.registers.<app>.schemas[]. Report each one that does not, by slug.
integriq's test is a working reference, and decidiq#1353 added an equivalent that reads through SettingsService::shippedRegisterDescriptor(), the same merge the importer uses, so the guard cannot pass over a register OpenRegister never sees. That detail matters: a guard reading the raw file rather than the merged descriptor would have missed the decidiq case.
Ship it as a warning first, per the standing rule. A fleet scan before enabling would be worth doing anyway, since three of the apps checked so far were affected and nobody has looked at the other eighteen.
Three apps hit this in one week, each with a different and expensive consequence, and no existing gate can see it.
The defect
An app declares a schema in a
register.d/*.jsonfragment. The register's owncomponents.registers.<app>.schemas[]list does not name it.ImportHandlerbuilds the register's schema ids by walking the declared list, so the schema is created and linked to nothing. Every read scoped to that register then misses it, and the failure is silent at every layer.The three instances
decidiq: 106 declared, 95 attached, 11 never attached, including
decision-template,approval-routeandapproval-action. Consequence:typeOf()set the schema, a register-scoped miss threw by design, acatch (\Throwable)logged it and returned null, and both the publish guard and the clause stamp were skipped. Every decision published with no legal remedy clause and answered 200 OK. Fixed in ConductionNL/decidiq#1353.portaliq: 7 never attached (
portalMandate,portalInvitation,portalReferenceLink,portalAccessRequest,portalFormBinding,portalIntakeSubmission,changeProposal). Their object routes 404. Compounded becauseinfo.versionwas frozen at 0.23.0, so no upgrade would ever have re-imported them. Fixed in ConductionNL/portaliq#622.integriq:
digitalPostMessagedeclared incomponents.schemasand in neither the register norSCHEMA_SLUGS, while the service writes the record of what was sent to that slug inside acatch (Throwable)that only logs. Every record of what was posted as digital post was dropped, silently, on every send. Fixed in ConductionNL/integriq#2082.Why nothing catches it
/api/schemassees the global list, where the schema exists. Only a register-scoped query reveals it.RegisterDescriptorTest::testRegisterDeclaresAllSchemaSlugs) and that is the only reason its instance was caught quickly. decidiq and portaliq had none.Proposed gate
Replay the app's own fragment merge, then assert every declared schema appears in
components.registers.<app>.schemas[]. Report each one that does not, by slug.integriq's test is a working reference, and decidiq#1353 added an equivalent that reads through
SettingsService::shippedRegisterDescriptor(), the same merge the importer uses, so the guard cannot pass over a register OpenRegister never sees. That detail matters: a guard reading the raw file rather than the merged descriptor would have missed the decidiq case.Ship it as a warning first, per the standing rule. A fleet scan before enabling would be worth doing anyway, since three of the apps checked so far were affected and nobody has looked at the other eighteen.