Repository navigation
Commit fa26308
fix(objectql,runtime): a hook whose handler names a function gets ctx.title in-process (#24423)
Part of #24289
Clause-②: yes (widening)
Item 1 of the card's release `6252025560`: a hook whose `handler` NAMES
a function now gets the same `ctx.title(field?)` in-process that an
inline handler and a sandboxed `body` get. Item 2 (declaring `title` on
`HookContext`) is **not** built here. P3 below found two doors that run
hooks in-process and still do not deliver `ctx.title`, so the
declaration would run ahead of the runtime. That half goes back to the
PM as `needs_decision`, and #24289 stays open for it. This PR carries no
closing keyword for that reason.
## What changed
- `packages/objectql/src/hook-binder.ts`: `BindHooksOptions` gains
`wrapHandler(handler, hook)`. `resolveHandler` applies it once per hook,
at bind, to every handler the hook runs in-process, after a string
`handler` resolves: an inline function, a name from the bind's
`functions`, or a name the same package registered on the engine (the
new `resolveInProcessHandler` is the old resolution, moved unchanged).
It is never applied to the handler `bodyRunner` builds from a `body`.
The engine awaits a handler and discards its return value, and so does
the adapter around the wrapped function. With no `wrapHandler`, every
handler binds exactly as resolved.
- `packages/spec/src/contracts/objectql-engine.ts`:
`IObjectQLEngine.bindHooks` opts gain `wrapHandler?: (handler: (context:
HookContext) => unknown, hook: Hook) => (context: HookContext) =>
unknown`, typed with the spec's own `Hook` and `HookContext` (no `any`,
no bare `unknown` for a known signature; `unknown` is only the discarded
return).
- `packages/objectql/src/engine.ts`: `ObjectQL.bindHooks` opts carry the
same member (`BindHooksOptions['wrapHandler']`). No engine default is
installed.
- `packages/runtime/src/app-artifact-handlers.ts`:
`bindAppArtifactHandlers` passes `wrapHandler: (handler, hook) =>
withInProcessHookTitle(handler, ql, String(hook.name))`. The pre-bind
copy of inline-handler hooks (`hookWithInProcessTitle`, PR #24356) is
removed. `withInProcessHookTitle` and `buildTitleSurface`
(`body-runner.ts`) are unchanged.
### Why the wrap is a binder option (G1)
The alternative was to resolve names in the runtime before the bind.
That would copy the binder's resolution rules (the bind's `functions`,
then a function the same package registered on the engine, never another
package's, with `INVALID_REFERENCE` for a miss) into a second resolver.
The binder is documented as the one resolver ("Inline function handlers
and string-named handlers share one resolver"). With the option, the
binder resolves both kinds and applies the one wrap after resolution, so
the inline handler and the named handler take the same wrap at the same
point. That is also why the runtime's earlier inline-only pre-wrap is
removed rather than kept beside it: keeping it would wrap an inline
handler twice.
## Premises, each measured
**P1 (true): a named handler ran in-process without `ctx.title`, on both
doors.** The new pin
`packages/verify/src/harness.named-hook-title-door.test.ts` was run
against `origin/main` `ee882b845` (built through turbo) before any fix:
```
× the artifact door: a string handler resolved from the runtime module
× the source door: a string handler resolved from the stack's functions
TypeError: ctx.title is not a function (both)
Tests 2 failed | 2 passed (4) (the 2 passing: PR #24356's inline-handler pins)
```
After the fix (head `043baeb3f`), same command: `Tests 4 passed (4)`.
**P2 (true):** on `ee882b845`, `withInProcessHookTitle` wrapped only an
inline `handler` in `bindAppArtifactHandlers` (`hookWithInProcessTitle`,
`typeof hook.handler !== 'function'` returns the hook unchanged), before
`ql.bindHooks`. A string `handler` reached the binder unwrapped. The P1
run above shows it: inline pins green, named pins red.
**P3 (false): not every in-process door delivers `ctx.title` after item
1.** Doors enumerated and measured at head `043baeb3f`:
| Door | Runs in-process? | `ctx.title` after this PR | Reading |
|:--|:--|:--|:--|
| sandboxed `body` (artifact door, `objectstack dev`) | no (QuickJS) |
yes | pinned, `harness.hook-title-door.test.ts` artifact case |
| inline `handler` via `bindAppArtifactHandlers` (source boot, `os
serve` over a config) | yes | yes | pinned,
`harness.hook-title-door.test.ts` source case and
`app-artifact-handlers.hook-title.test.ts` |
| named `handler`, source door (`functions`) | yes | yes | pinned, new
verify pin |
| named `handler`, artifact door (`runtimeModule`) | yes | yes | pinned,
new verify pin |
| `ObjectQLPlugin`'s metadata-service bind (`ql.bindHooks(bindable, {
packageId: 'metadata-service' })`) | bodies no; a handler only by a name
owned by `metadata-service` | bodies yes (engine default body runner);
no wrap passed | read from code: stored rows are JSON, and no caller
registers functions under `metadata-service` |
| `ql.registerHook(event, fn)`, documented as the plugin escape hatch
(`data-hooks.md`, method 2) | yes | **no** | measured: `typeof ctx.title
=== 'undefined'` |
| an `ObjectQL` used directly, `ql.bindHooks([...])` with no runtime (a
host or a test harness) | yes | **no** (by design: no `wrapHandler`) |
measured: `typeof ctx.title === 'undefined'` for an inline and a named
handler |
The two measured `undefined` rows came from one scratch vitest run in
`packages/objectql` against a bare `new ObjectQL()`:
`{"registerHook":"undefined","bindInline":"undefined","bindNamed":"undefined"}`.
The scratch file was deleted right after; it was never committed. So
`HookContext` gains no `title` here. Declaring it, whether required or
optional, would declare a member that the documented `registerHook` door
does not deliver. The options go to the PM.
## Pins
- `packages/verify/src/harness.named-hook-title-door.test.ts` (new, G2):
on both `bootStack` doors, a named handler answers `ctx.title()` = `L1 ·
Widget` (this record's formula title, one hop through a lookup),
`ctx.title('product')` = `Widget (W-1)`, and `ctx.title('spare')` =
`null`. These are the same answers PR #24356's pin gets from a `body`.
- `packages/objectql/src/hook-binder-wrap-handler.test.ts` (new):
- the inline handler, the name from the bind's `functions` and the
engine-registered name of the same package all take the wrap, once each,
at bind;
- a `body`'s handler never takes it;
- **control:** with no `wrapHandler`, a named handler and an inline
handler are called with the engine's own context object (identity), as
before.
## Ablation (G2)
One run under trap restore, after the fix was committed (`39cf701a1`):
- **Mutation.** The `wrapHandler:` line was deleted from
`packages/runtime/src/app-artifact-handlers.ts` through
`scripts/ablation-replace.mjs`: anchor 1 → 0, blob `1b658c88ace0` →
`376387b42a8a`.
- **Rebuild.** Runtime was rebuilt. The build exited 1 in its
declaration pass, because `noUnusedLocals` flags the now-unused
`withInProcessHookTitle` import. That happens after the JS emit, so the
marker count in `dist/index.js` went 1 → 0, and
`ablation-dist-preflight.mjs runtime 'wrapHandler: (handler, hook)'
--absent` exited 0.
- **Ablated run.** The verify pins went `3 failed | 1 passed`: both
named-handler doors and the inline source door failed with `TypeError:
ctx.title is not a function`, and the artifact `body` door stayed green.
The inline door going red is expected: inline handlers now take the same
one wrap.
- **Restore.** `git checkout HEAD -- ABS_PATH` gave blob `1b658c88ace0`,
equal to the HEAD blob, and `git diff HEAD` was empty. Runtime was
rebuilt (exit 0, marker count back to 1) and the pins were `4 passed
(4)`.
## Local verification (head `043baeb3f`)
- Build closure: `turbo run build --filter=@objectstack/verify...` (49
tasks), then the 4 touched packages, then
`--filter='!@objectstack/docs'` for `check:dual-build-cjs-loads`.
- Derived gates (`dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no paths): 92 families.
- 89 passed with exit 0 on the first pass.
- 3 first refused on prerequisites and then passed, exit 0:
- `check-engine-split-ratio --days 90` (exit 2) and
`check-plugin-teardown-shape --self-test` (exit 3) on the shallow clone;
both passed after `git fetch --unshallow origin main`.
- `check:dual-build-cjs-loads` (exit 3, no dist); passed after the full
build: "111 published require entry point(s) across 69 package(s) load".
- Typecheck (each `pnpm --filter PKG typecheck`, test layer included):
spec, runtime and verify passed with exit 0. objectql first exited 1:
the new pin's context literal lacked `ql`, which gave TS2352 under
`tsconfig.test.json`. That is fixed in `a2263206e`, and objectql then
exited 0 ("test layer compiles").
- Tests:
- objectql: `vitest run` 446 files, 8362 passed.
- runtime: `vitest run --project local` 421 files, 6626 passed, 19
skipped.
- verify: `vitest run` 43 files, 371 passed.
- `eslint --no-inline-config` over the 6 changed `.ts` files: exit 0.
- Final head `a2263206e` changes only that test literal. `--commands`
derives the same 92 families there. The test-facing gates
(`check:cross-package-test-inputs`, `check:test-source-alias`,
`check:nul-bytes`, `check:tier-file-adoption`) were re-run there with
exit 0, and `--ran` reconciles 92 derived, 92 run, 0 NOT-MEASURED, 0
UNRUN.
## Acceptance notes
- Item 2 (`HookContext.title`) is held for a decision; the reading and
the options are in the os-dev-report on #24289.
- `@objectstack/verify` changes only a test file (its `files[]` ships
`dist` only), so it takes no changeset entry.
Dev session: `session_01UxK7vXeLCgYoxSXaHuQA1Y` (`mode:cloud`),
dispatched by the `domain:spec` seat 2
(`session_019oFY6Hi35J7atvgVFnmoaK`) for claim `6257131238`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01UxK7vXeLCgYoxSXaHuQA1Y
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UxK7vXeLCgYoxSXaHuQA1Y)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8ed3786 commit fa26308
7 files changed
Lines changed: 297 additions & 24 deletions
File tree
- .changeset
- packages
- objectql/src
- runtime/src
- spec/src/contracts
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
290 | 290 | | |
291 | 291 | | |
292 | 292 | | |
293 | | - | |
| 293 | + | |
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
| |||
4866 | 4866 | | |
4867 | 4867 | | |
4868 | 4868 | | |
| 4869 | + | |
| 4870 | + | |
4869 | 4871 | | |
4870 | 4872 | | |
4871 | 4873 | | |
| |||
| 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 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
61 | 77 | | |
62 | 78 | | |
63 | 79 | | |
| |||
392 | 408 | | |
393 | 409 | | |
394 | 410 | | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
395 | 426 | | |
396 | 427 | | |
397 | 428 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | | - | |
259 | | - | |
260 | | - | |
261 | 258 | | |
262 | 259 | | |
263 | 260 | | |
| |||
277 | 274 | | |
278 | 275 | | |
279 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
280 | 282 | | |
281 | 283 | | |
282 | 284 | | |
| |||
339 | 341 | | |
340 | 342 | | |
341 | 343 | | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
347 | | - | |
348 | | - | |
349 | | - | |
350 | | - | |
351 | | - | |
352 | | - | |
353 | | - | |
354 | | - | |
355 | | - | |
356 | | - | |
357 | | - | |
358 | | - | |
359 | | - | |
360 | | - | |
361 | 344 | | |
362 | 345 | | |
363 | 346 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| |||
403 | 404 | | |
404 | 405 | | |
405 | 406 | | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
406 | 421 | | |
407 | 422 | | |
408 | 423 | | |
| |||
| 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 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
0 commit comments