You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split out of #3856 / objectui#2990, which hit this directly and documented it rather than working around it silently.
The mechanism
ActionDef (packages/core/src/actions/ActionRunner.ts:277, interface at :78) ends with:
/** Any additional properties */[key: string]: any;
at ActionRunner.ts:185. So ActionDef accepts any key of any type. Concretely, in objectui#2990:
deleting ActionDef.execute produced zero compile errors, even though the field had just been removed from the interface;
stale metadata still authoring execute: 'markDone' continues to type-check today;
the same deletion on @object-ui/types' ActionSchema — which has no index signature — correctly produced TS2353 at the authoring site.
That asymmetry is the whole issue: one of the two readers is capable of catching a retired key, the other is structurally incapable.
Because the compiler couldn't help, objectui#2990 had to add a runtime branch to keep the removal audible — executeScript now returns the rename prescription when it finds only the retired key, rather than a bare "No script provided". That branch exists solely to compensate for this index signature. Binding no handler silently is the #2169 "Mark Done does nothing" shape, so the compensation was necessary — but it is compensation.
Not the same as ActionContext's
ActionContext has an index signature too (ActionRunner.ts:53) and it should keep it: it is a runtime data bag whose keys are genuinely open (data, record, pageVariables, user, plus whatever a host passes). ActionDef is the opposite — it is a declared metadata contract that mirrors @objectstack/spec's ActionSchema. An open key set on a contract is what lets a typo (targt, exectue) and a tombstoned key (execute) both sail through to a runner that then silently does nothing.
Why it can't just be deleted
The runner deliberately accepts shapes wider than current spec, and ActionDef already enumerates a lot of them explicitly (actionType "legacy action type field", api as string | ApiConfig, onClick "legacy", navigate?: any). Some callers likely pass keys not declared anywhere. A bare removal would surface as a wall of TS2353 across components / plugin-* / host apps, some legitimate.
Staged narrowing is the realistic path:
Inventory what is actually passed — grep the repo's ActionDef construction sites and any action[...] dynamic reads, and add a dev-mode warning on unrecognized keys (the "declared, warned, lintable shim" form AGENTS.md PD Add comprehensive test suite for Zod schema validation #12 asks for, instead of a bare open type).
Promote every legitimate key found into an explicit optional field, keeping the legacy ones marked @deprecated.
#3903 established that the spec contract stops at authored source — stored sys_metadata rows are rehydrated unparsed. This is the same "declared ≠ enforced" pattern on the type channel rather than the parse channel: @object-ui/core is the reader that consumes those unparsed rows (runner(action)), and its type accepts anything they contain. The two issues bound the same gap from opposite ends; fixing #3903 alone would still leave a reader that cannot describe what it accepts.
Blocked-by: #5970
Split out of #3856 / objectui#2990, which hit this directly and documented it rather than working around it silently.
The mechanism
ActionDef(packages/core/src/actions/ActionRunner.ts:277, interface at:78) ends with:at
ActionRunner.ts:185. SoActionDefaccepts any key of any type. Concretely, in objectui#2990:ActionDef.executeproduced zero compile errors, even though the field had just been removed from the interface;execute: 'markDone'continues to type-check today;@object-ui/types'ActionSchema— which has no index signature — correctly producedTS2353at the authoring site.That asymmetry is the whole issue: one of the two readers is capable of catching a retired key, the other is structurally incapable.
Because the compiler couldn't help, objectui#2990 had to add a runtime branch to keep the removal audible —
executeScriptnow returns the rename prescription when it finds only the retired key, rather than a bare "No script provided". That branch exists solely to compensate for this index signature. Binding no handler silently is the #2169 "Mark Done does nothing" shape, so the compensation was necessary — but it is compensation.Not the same as
ActionContext'sActionContexthas an index signature too (ActionRunner.ts:53) and it should keep it: it is a runtime data bag whose keys are genuinely open (data,record,pageVariables,user, plus whatever a host passes).ActionDefis the opposite — it is a declared metadata contract that mirrors@objectstack/spec'sActionSchema. An open key set on a contract is what lets a typo (targt,exectue) and a tombstoned key (execute) both sail through to a runner that then silently does nothing.Why it can't just be deleted
The runner deliberately accepts shapes wider than current spec, and
ActionDefalready enumerates a lot of them explicitly (actionType"legacy action type field",apiasstring | ApiConfig,onClick"legacy",navigate?: any). Some callers likely pass keys not declared anywhere. A bare removal would surface as a wall ofTS2353acrosscomponents/plugin-*/ host apps, some legitimate.Staged narrowing is the realistic path:
ActionDefconstruction sites and anyaction[...]dynamic reads, and add a dev-mode warning on unrecognized keys (the "declared, warned, lintable shim" form AGENTS.md PD Add comprehensive test suite for Zod schema validation #12 asks for, instead of a bare open type).@deprecated.tsccatches both typos and retired spec keys, and Security: attachment blob download has no record-level RLS — bytes served by fileId with only a committed-status check (IDOR) #2990's runtime prescription branch can eventually retire with it.Step 1 is independently useful and non-breaking: it turns an invisible failure into a warning without changing types.
Relation to #3903
#3903 established that the spec contract stops at authored source — stored
sys_metadatarows are rehydrated unparsed. This is the same "declared ≠ enforced" pattern on the type channel rather than the parse channel:@object-ui/coreis the reader that consumes those unparsed rows (runner(action)), and its type accepts anything they contain. The two issues bound the same gap from opposite ends; fixing #3903 alone would still leave a reader that cannot describe what it accepts.Refs #3856 (objectui#2990), #3903, #2169, #3855, #3883, AGENTS.md PD #12.