Skip to content

Commit 26f18cd

Browse files
committed
chore(spec): regenerate the registry and declaration surface for the review fixes
`gen:migration-registry` picks up the corrected acceptance criteria (a type check alone proves nothing for a handler the type never reached); `gen:api-surface-declarations` picks up the member doc, which now states the runtime half and why a null-valued key is the load-bearing case. `check:generated`: all 16 artifacts up to date. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77f73a3 commit 26f18cd

2 files changed

Lines changed: 37 additions & 16 deletions

File tree

‎packages/spec/api-surface-declarations/ui.txt‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,31 @@ interface ActionEngineFacade {
177177
*
178178
* ## What the type refuses, measured
179179
*
180-
* A bare filter no longer type-checks, on BOTH paths a caller can reach it
181-
* by: an object literal (`{ status: 'completed' }`) fails the excess-property
182-
* check, because a field name is not an envelope key; and a filter held in a
183-
* variable typed `FilterCondition` fails TS2559 — `EngineQueryOptions` is a
184-
* weak type, every key optional, and a filter of field names has no property
185-
* in common with it. The refusal is a compile error at the call site, never a
186-
* runtime surprise. The envelope's own keys are typed, so `where: 'a = b'`,
187-
* `fields: 'id,subject'` and `limit: '50'` are refused too.
180+
* A bare filter no longer type-checks, on BOTH paths a TYPED caller can
181+
* reach it by: an object literal (`{ status: 'completed' }`) fails the
182+
* excess-property check, because a field name is not an envelope key; and a
183+
* filter held in a variable typed `FilterCondition` fails TS2559 —
184+
* `EngineQueryOptions` is a weak type, every key optional, and a filter of
185+
* field names has no property in common with it. The envelope's own keys are
186+
* typed, so `where: 'a = b'`, `fields: 'id,subject'` and `limit: '50'` are
187+
* refused too.
188+
*
189+
* ## …and what refuses it for a caller the TYPE never reached
190+
*
191+
* `buildActionEngineFacade` returns `any`, so a handler in a JS config, one
192+
* annotated with a local copy of this context, or a `(ctx: any)` handler is
193+
* bound by nothing here. For those the runtime arm refuses the withdrawn
194+
* shape itself, before the engine, carrying the same prescription
195+
* (`ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION`,
196+
* `packages/runtime/src/action-execution.ts`) and reading its key set off
197+
* THIS schema so the two channels cannot drift apart.
198+
*
199+
* ⚠️ That arm is load-bearing rather than belt-and-braces, and the reason is
200+
* a `null`: the engine's own unknown-option refusal (#4371) exempts a
201+
* `null`-VALUED key, because on an option bag a `null` is a withdrawal. On a
202+
* FILTER it is the "rows with no X" idiom — so `{ deleted_at: null }` passed
203+
* straight through would be dropped unexecuted and the read would widen to
204+
* EVERY row, silently, to a caller whose next line is often a delete.
188205
*
189206
* ## `context` is the caller's to pass and NOT the caller's to choose
190207
*

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5612,14 +5612,18 @@ const step18: MigrationStep = {
56125612
+ 'asserts a vocabulary fact the spec declares nowhere, reserving the field name `where` across '
56135613
+ 'every customer\'s data model to buy one parameter\'s compile-time check.',
56145614
acceptanceCriteria:
5615-
'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope, and the package '
5616-
+ 'type-checks: a bare filter is now a compile error at the call site — an object literal fails '
5617-
+ 'the excess-property check and a `FilterCondition` variable fails TS2559 — so `tsc --noEmit` '
5618-
+ 'over the handlers finds every unmigrated call, with no runtime run needed. Then confirm the '
5619-
+ 'reads that were already SILENTLY EMPTY: any handler that had been passing the envelope was '
5620-
+ 'resolving to `[]` on every call, so a suite written against the mistake passed and the row '
5621-
+ 'count is the only witness — re-run each migrated handler against seeded data and assert it now '
5622-
+ 'returns the rows its filter selects, rather than asserting it still resolves.',
5615+
'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope. Where the handler '
5616+
+ 'is annotated with the PUBLISHED `ActionHandlerContext`, `tsc --noEmit` finds every unmigrated '
5617+
+ 'call on its own — a bare filter is a compile error there, an object literal failing the '
5618+
+ 'excess-property check and a `FilterCondition` variable failing TS2559. ⚠️ Where it is NOT — a '
5619+
+ 'handler in an `objectstack.config.js` / `.mjs`, one annotated with a local copy of the context '
5620+
+ 'type, or a `(ctx: any)` handler — the type reaches nothing and a type-check alone proves '
5621+
+ 'nothing: those callers are refused at RUNTIME by the facade arm, with the same prescription, so '
5622+
+ 'the migration is complete for them only once each such handler has actually been RUN. Then '
5623+
+ 'confirm the reads that were already SILENTLY EMPTY: any handler that had been passing the '
5624+
+ 'envelope was resolving to `[]` on every call, so a suite written against the mistake passed and '
5625+
+ 'the row count is the only witness — re-run each migrated handler against seeded data and assert '
5626+
+ 'it now returns the rows its filter selects, rather than asserting it still resolves.',
56235627
},
56245628
{
56255629
id: 'address-location-value-unknown-keys-refused',

0 commit comments

Comments
 (0)