Filed out of scope from #15079 (the runtime executor for the declarative row-level operation: 'update' action, ruled on #14092). Found while auditing whether the card's list of four type-only readers is complete — it is not, and this is the fifth.
What happens
reconcileActionRegistrations in packages/objectql/src/action-governance.ts builds the unboundDeclarations finding — "a declared script action with no body and no handler under any candidate key: a button wired to nothing" — from a type-only test:
for (const { action, objectName, storeKey } of declarations) {
if ((action?.type ?? 'script') !== 'script') continue; // only script needs a handler
if (action?.body) continue; // its handler is synthesized
A declarative update action is exactly that shape and is meant to be: ActionSchema refuses target and body beside operation: 'update', and its type stays at the materialized default 'script' because the platform action route IS where the write is performed. So every such action falls through both continues and lands in unboundDeclarations.
runActionGovernanceInventory then logs, at every boot and on every metadata:reloaded:
[action-governance] declared script actions with NO handler — a button wired to
nothing (ADR-0078); add a `body`, or register a handler under the declared `target`
Once the runtime executor lands, that sentence is false for these actions and its prescription is actively wrong: adding a body or a target is refused at parse time for an operation: 'update' action.
Why it is not just noise
The inventory is the ADR-0110 D5 governance surface an operator reads to find real dead buttons. A population that is never actionable and grows with every declarative update action an app author writes is how a diagnostic stops being read — and the undeclared-handler 404 on the /actions door cites [action-governance] by name as the place to look.
Blast radius, measured
- Warn-only.
runActionGovernanceInventory is documented as "warn-only and exception-proof"; nothing refuses a boot or a dispatch on it, and the route dispatches the update correctly regardless. No behaviour is wrong — the diagnostic is.
- It reaches every host that boots the engine plugin, once per boot plus once per metadata reload whose fingerprint changed.
The fix, in one line of the same shape the runtime took
Read operation before type, as the runtime doors now do:
if (action?.operation === 'update') continue; // the platform performs the write
@objectstack/objectql is where the reader lives, so this is that package's card. The runtime side deliberately did not widen into it: the executor does not need this seam, and #15079's file surface says to report rather than widen silently.
Refs
#15079 (the runtime executor) · #14092 (the ruling) · PR #15077 (the spec half, where operation/patch land) · ADR-0110 D5 (the inventory) · ADR-0078
Filed out of scope from #15079 (the runtime executor for the declarative row-level
operation: 'update'action, ruled on #14092). Found while auditing whether the card's list of fourtype-only readers is complete — it is not, and this is the fifth.What happens
reconcileActionRegistrationsinpackages/objectql/src/action-governance.tsbuilds theunboundDeclarationsfinding — "a declaredscriptaction with nobodyand no handler under any candidate key: a button wired to nothing" — from atype-only test:A declarative update action is exactly that shape and is meant to be:
ActionSchemarefusestargetandbodybesideoperation: 'update', and itstypestays at the materialized default'script'because the platform action route IS where the write is performed. So every such action falls through bothcontinues and lands inunboundDeclarations.runActionGovernanceInventorythen logs, at every boot and on everymetadata:reloaded:Once the runtime executor lands, that sentence is false for these actions and its prescription is actively wrong: adding a
bodyor atargetis refused at parse time for anoperation: 'update'action.Why it is not just noise
The inventory is the ADR-0110 D5 governance surface an operator reads to find real dead buttons. A population that is never actionable and grows with every declarative update action an app author writes is how a diagnostic stops being read — and the undeclared-handler 404 on the
/actionsdoor cites[action-governance]by name as the place to look.Blast radius, measured
runActionGovernanceInventoryis documented as "warn-only and exception-proof"; nothing refuses a boot or a dispatch on it, and the route dispatches the update correctly regardless. No behaviour is wrong — the diagnostic is.The fix, in one line of the same shape the runtime took
Read
operationbeforetype, as the runtime doors now do:@objectstack/objectqlis where the reader lives, so this is that package's card. The runtime side deliberately did not widen into it: the executor does not need this seam, and #15079's file surface says to report rather than widen silently.Refs
#15079 (the runtime executor) · #14092 (the ruling) · PR #15077 (the spec half, where
operation/patchland) · ADR-0110 D5 (the inventory) · ADR-0078