Repository navigation
Commit a6f3a13
fix(verify)!: a hooks.run delete resolves with what the engine's delete answers, not a row (#22830)
Fixes #22599
Clause-②: no (narrowing)
## What
`stack.hooks.run(object, 'delete', { id }, opts)` in
`@objectstack/verify` was declared to resolve with a row (a Promise of
`EngineRow`, i.e. `Record` of string to any), for a person (`{ as }`)
and for the system (`{ system: true }`). The engine's `delete` answers
`true` for the by-id row it removed. So a test that read a field of a
delete's answer type-checked, then read `undefined`.
This PR types the `delete` answer as what the engine answers, by
direction A of the triage grade (comment 6093534577). It does not change
any behaviour (direction B, resolving with the deleted row's pre-image,
is not taken).
## Premise, re-measured first on `main` `b7cd1af9df`
A temporary probe (deleted, never committed) booted the handle fixture
through `bootStackOnce` and ran one by-id delete through `hooks.run` as
each caller kind. Output line: `PROBE22599
{"person":true,"personType":"boolean","system":true,"systemType":"boolean"}`
(1 test passed; `VERDICT command-exit 0`). The premise holds for both
callers.
## The change (`packages/verify/src/handle.ts`)
- `EngineDeleteAnswer`: the engine's own declared return of
`ObjectQL['delete']`, awaited, read off the method with `ReturnType` and
`Awaited` rather than re-declared. This follows the precedent
`AutomationCondition` sets in the same file (read off
`AutomationEngine['evaluateCondition']`). The engine declares `boolean |
number`: `boolean` for a by-id delete, `number` for a predicate delete.
No separate by-id type is exported, and an `afterDelete` hook may
substitute a count on a by-id delete (the engine's `isDeleteResultShape`
accepts either). So the door promises the engine's declaration, not a
hand-written `boolean`.
- `HookWriteAnswer`, keyed on the operation: the row for `insert` /
`update` (unchanged, `EngineRow`), `EngineDeleteAnswer` for `delete`. It
is a distributive conditional type.
- Both `hooks.run` overloads (the person one and the #22301 system one)
are now generic over the operation (`Op` constrained to `'insert' |
'update' | 'delete'`) and resolve with `HookWriteAnswer` of `Op`. The
implementation is untouched: each `case` still returns the engine call
unchanged.
- Docblocks say what each write resolves with, and that a test needing
the deleted row reads it before the delete.
Neither new alias is exported, the same as `AutomationCondition`. The
rebuilt `dist/index.d.ts` and `dist/index.d.cts` carry both as local
declarations, and the one new import there is `ObjectQL` from
`@objectstack/objectql`, a declared runtime dependency of this package.
## PM mechanism assumptions, measured
1. **Split overloads vs a non-literal operation.** Measured standalone
with `tsc --strict`: splitting the `delete` literal into its own
overloads makes a call whose `operation` is the union type a compile
error (`TS2769: No overload matches this call … '"delete"' is not
assignable to '"update" | "insert"'`). The generic form keeps that call
compiling, and it resolves with `EngineRow | boolean | number`, the
union of the answers. That is why the overloads are generic rather than
split. The type test pins the union resolution.
2. **Callers of a delete's answer.** All 66 `hooks.run(` call sites in
this repo pass a literal operation. The `'delete'` ones all live in
`packages/verify` tests and await the answer without reading it (or pass
it to `refusalOf`, typed as a Promise of unknown). Downstream sweep
after building the closure: `pnpm turbo run typecheck
--filter='...@objectstack/verify' --concurrency=2 --continue` (the `...`
prefix = verify and every downstream consumer), 74 of 74 tasks
successful. All 9 typecheck tasks were cache misses and executed: cli,
dogfood, downstream-contract, example-crm, example-multi-package,
example-showcase, example-todo, organizations, verify.
`@objectstack/verify:build` was a cache miss too, so they read the
rebuilt `.d.ts`. No caller needed a change.
3. **The type test can fail.** See the reverse checks below.
## Tests
- New `packages/verify/src/handle.delete-answer.test.ts`, a type test.
It is compiled by the package's test-layer program
(`tsconfig.test.json`, run by `check:test-typecheck` inside `pnpm
--filter @objectstack/verify typecheck`). `tsc --listFiles -p
tsconfig.test.json` names it once (control: `handle.test.ts` named
once), and the build program `tsconfig.json` names it 0 times, as
designed. It pins:
- a delete answer equal to the engine's declared delete answer, for a
person and for the system;
- no `id` property on it;
- insert and update still `EngineRow`, both callers;
- the union operation resolving to the union of the answers.
- `pnpm --filter @objectstack/verify typecheck`: exit 0.
`check:test-typecheck: OK … 0 file(s) / 0 error(s)`.
- `pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2`:
`Test Files 31 passed (31)`, `Tests 246 passed (246)`. 31 test files are
on disk (30 at the base plus the new one).
### Reverse check 1: the type test against the old declaration
(committed first, at `b9399f614`)
`node scripts/ablation-replace.mjs --file packages/verify/src/handle.ts
--expect 2` put the old return type back on both overloads. The anchor
was the new return annotation (a Promise of `HookWriteAnswer` of `Op`)
and the replacement was the old one (a Promise of `EngineRow`),
described in words here because this body's sanitizer deletes generic
types. The tool reported the anchor count going from 2 to 0, and the
blob going from `9f3fd264f2be` to `68cc306a9eb7`. Run against it, `tsc
--noEmit -p packages/verify/tsconfig.test.json` went red, in the
expected direction:
- lines 37 and 38, the delete answer for a person and for the system:
TS2344;
- lines 42 and 43, `not.toHaveProperty('id')`: TS2554;
- line 54, the union operation: TS2344;
- `handle.ts` line 128: TS6196, `HookWriteAnswer` unused (a by-product
of the mutation).
The insert and update assertions stayed green. Through `pnpm --filter
@objectstack/verify check:test-typecheck` the same mutation read `2
problem(s)`, exit 1. On restore, the tool reported `blob == HEAD
(9f3fd26) and git diff HEAD is empty`.
### Reverse check 2: a consumer reads the rebuilt `.d.ts`
A temporary file in `packages/qa/dogfood/test/` read `.id` off a system
delete's answer. dogfood's program resolves `@objectstack/verify`
through its `exports`, so through `dist`. `tsc --noEmit -p
packages/qa/dogfood/tsconfig.json` answered `TS2339: Property 'id' does
not exist on type 'number | boolean'`, its only error. The file was
removed by a trap, and `git status --porcelain` came back empty.
## Gates
- Union of the dispatch-derived list and the re-derivation from this
diff (`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, 63 commands: all 46 named at dispatch, plus
17 this diff adds, from the changeset and the new test). I ran it once,
after the final commit, on `27331626c`, each exit code recorded before
any pipe, and every one exited 0. `node scripts/pm/dispatch-gates.mjs
--ran` reported `63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN`. Two of
them first answered `PREREQUISITE NOT MET` (exit 3), and both went green
once the prerequisite was met:
- `check-plugin-teardown-shape.mjs --self-test` could not read its
pinned positive-control commit on this shallow clone. Fixed by fetching
that one commit.
- `check:dual-build-cjs-loads` found six packages outside verify's
closure with no `dist/`. Fixed by building exactly those six.
- `pnpm lint` (the lane blind spot): run as a proven narrowing, not as
the full scan.
- Population, from eslint's own config: of the 3 changed paths, eslint
lints the 2 `.ts` files; the `.changeset` file answers `File ignored
because no matching configuration was supplied`.
- Count, from `--format json`: 3 results; the 2 linted files have 0
errors and 0 warnings.
- Invariance: `eslint.config.mjs` never enables type-aware linting.
Every `parserOptions` is `{ ecmaVersion, sourceType }`, with no
`project` and no `projectService`. Its four custom plugins
(`slot-lookup`, `query-options`, `verify-stand-in`, `comment-swallow`)
judge one file's own AST and scope, and the only files it reads are two
baselines this diff does not touch. So this diff cannot move the verdict
of any untouched file. The full `pnpm lint` is CI's.
## Changeset
`.changeset/22599-verify-hooks-run-delete-answer.md`,
`@objectstack/verify` `minor`. It is marked **BREAKING** under the
launch-window convention and carries `Clause-②: no (narrowing)`. Its
ADR-0087 disposition is `not-required (runtime-interface-only
packages/verify/src/handle.ts#VerifyHandle)`, which
`check:adr-0087-registration` verified
(`[BREAKING+bang+clause-②-narrowing] … verified:
packages/verify/src/handle.ts#VerifyHandle (interface)`).
**Note for the contract review: the migration text is prose, not a FROM
→ TO label.** The dispatch asked for FROM → TO and the one-line fix, and
the changeset states both: the old declared type, the new one, and the
assertion to write instead. It does not use the `FROM … → … TO` label
convention, because:
- every ADR-0087 category this change can honestly claim refuses a body
the prescription detector reads as a migration;
- `runtime-interface-only` inherits that refusal;
- `type-surface-only` is exempt from it, but its predicate 4 reads the
base type `EngineRow` as a concrete named type, not erased;
- `registered` would need a `packages/spec` ledger entry for a
TypeScript-only surface `objectstack migrate meta` cannot reach.
This is the shape of the landed
`22345-run-provenance-context-retired.md`. The marker itself says the
body tells a TypeScript author what to assert instead.
## Acceptance notes
- **`update` has the same gap, out of scope here.** The ruling keeps
`insert` and `update` on `EngineRow`. The engine's `update` declares
`Record` of string to any `| number | null`: `null` when a by-id write's
readback leaves the caller's row scope, and a count on the predicate
path, which `hooks.run` never takes. Not measured through the door here;
recorded for whoever next touches these overloads.
- `packages/verify/README.md` shows a system delete without reading its
answer. It says nothing false, so it is unchanged.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01B5CHJNXuuqzChM4w6hkTN4)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d8c7d38 commit a6f3a13
3 files changed
Lines changed: 114 additions & 5 deletions
File tree
- .changeset
- packages/verify/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
110 | 130 | | |
111 | 131 | | |
112 | 132 | | |
| |||
254 | 274 | | |
255 | 275 | | |
256 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
257 | 281 | | |
258 | 282 | | |
259 | 283 | | |
| |||
264 | 288 | | |
265 | 289 | | |
266 | 290 | | |
267 | | - | |
| 291 | + | |
268 | 292 | | |
269 | | - | |
| 293 | + | |
270 | 294 | | |
271 | 295 | | |
272 | | - | |
| 296 | + | |
273 | 297 | | |
274 | 298 | | |
275 | 299 | | |
| |||
289 | 313 | | |
290 | 314 | | |
291 | 315 | | |
292 | | - | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
293 | 322 | | |
294 | 323 | | |
295 | 324 | | |
| |||
694 | 723 | | |
695 | 724 | | |
696 | 725 | | |
697 | | - | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
698 | 730 | | |
699 | 731 | | |
700 | 732 | | |
| |||
0 commit comments