Repository navigation
fix(runtime): an in-process hook handler is handed the same ctx.title a hook body gets - #24356
Conversation
… a hook body gets A source boot (verify's bootStack(config), os serve over a config) runs a hook's inline handler in-process, where ctx.title was absent: the handler threw "ctx.title is not a function" while the artifact's body answered. bindAppArtifactHandlers now hands each inline handler function a view of the engine's hook context that answers title through buildTitleSurface, the one accessor the sandbox installs. Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…process-hook-ctx-title
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1fecc564b345001df055f3a47da06642e8998440 && git checkout 1fecc564b345001df055f3a47da06642e8998440
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e4bcced0b451ecffd56689229e7d9842f5738a0b a14895b7178ae756b2892ac8e79362dfd6416e30 && git checkout -B drift-repro e4bcced0b451ecffd56689229e7d9842f5738a0b && git merge --no-ff a14895b7178ae756b2892ac8e79362dfd6416e30
node scripts/docs-audit/affected-docs.mjs --json e4bcced0b451ecffd56689229e7d9842f5738a0b
|
席位核对 — REWORK (patch round 1: the closing keyword only)
The code is accepted as it stands:
Owed in this PR: the closing keyword.
Settled:
Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Part of #24289
Clause-②: no
Basis for the Clause-② line:
ctx.title(field?), which the served runtime already gives a hookbody, is now also given to a hook's inlinehandlerfunction on the in-process path. No request is newly accepted. No published TypeScript type gains a member:HookContextin@objectstack/specis unchanged. No package entry exports anything new:withInProcessHookTitleis a module export ofpackages/runtime/src/sandbox/body-runner.ts, and neithersandbox/index.tsnor the packageindex.tsre-exports it.What changed
packages/runtime/src/sandbox/body-runner.ts:withInProcessHookTitle(handler, ql, hookName)wraps a hook handler function. The function is called with a view of the engine's hook context that answerstitle, and the answer comes frombuildTitleSurface, the same accessor the sandbox installs for abody. Its read channel and formula semantics (A formula field that reads through a reference (record.book.title) passes validateExpression, then reads null on every row: the read path projects only the row's own columns #24197, PR fix(runtime,objectql): ctx.title() answers a title formula over lookups — the one hop is served on the body's own channel #24307) are not changed. The one edit to it is its return-type annotation (NonNullable), because it always returns the function. The view is a Proxy: every other read and every write goes to the engine's own context, soctx.inputandctx.resultwrites land where the engine reads them, and the context object gains no key.packages/runtime/src/app-artifact-handlers.ts:bindAppArtifactHandlers, the one binder of an app's handlers, gives each hook whosehandleris a function a copy with that function wrapped, beforeql.bindHooks. The app's declaration is not rewritten. The engine's binder still decides whetherbodyorhandlerruns.null(never the record's id) when there is no title. The read channel is the hook context's ownctx.api, read at call time, so arunAshook reads under its declared identity. Two rules stay body-only: the reference form has noapi.readgate (an in-process handler already holdsctx.api), and the stored-metadata family is not refused.Measured
packages/runtime/src/sandbox/body-runner.tsandpackages/runtime/src/app-artifact-handlers.tswere restored toec749fbb4, hash-equal to the base blobs. Runtime was rebuilt, andwithInProcessHookTitlewas absent frompackages/runtime/dist(scripts/ablation-dist-preflight.mjs ... --absent, exit 0). Then the new verify pin went red on the source door only, withTypeError: ctx.title is not a function(1 failed, 1 passed; the artifact door answered). The new runtime pin went red on both cases with the same TypeError. Both files were restored to HEAD (hash-equal,git diff HEADempty) and runtime was rebuilt, with the marker present indist/index.jsanddist/index.cjs. Both pins were then green. This was one run, recorded here and not kept as a test.bindHooksToEngine, passed through the runtime's bind call. That option cannot be typed without editing two files outside this card's scope. The runtime callsql.bindHooksthroughIObjectQLEngine, whose options type is inpackages/spec/src/contracts/objectql-engine.ts.ObjectQL.bindHooksdeclares its own options inpackages/objectql/src/engine.ts. So the wrap happens inpackages/runtimeat bind time, andpackages/objectqlandpackages/specare not touched.os serveover a config (A4). This goes through the same code, so it is covered here.packages/cli/src/commands/serve.tsmountsnew AppPlugin(config, ...)when the config declares metadata and no AppPlugin is already present (line 3591 atec749fbb4).AppPlugin.startbinds throughbindAppArtifactHandlers(packages/runtime/src/app-plugin.ts, line 1683). This was read from the code; noos serveprocess was started.HookContext(HookContextSchema,packages/spec/src/data/hook.zod.ts) declares notitle, and this change does not add one. AdefineStackinline handler's parameter is untyped (HookSchema.handleris a(...args: any[]) => anycustom), soctx.title()compiles there. A handler annotated(ctx: HookContext)gets TS2339 (measured withtsc --strictagainst the built spec ata14895b71). This is reported to the PM and not changed here.Tests
packages/verify/src/harness.hook-title-door.test.ts(new) runs the samebeforeInsertstatements as an inline handler on a source boot and as a body on the artifact door, which is the shapeobjectstack devserves. Both doors answer{ own: 'L1 · Widget', product: 'Widget (W-1)', spare: 'null' }. That is this record's one-hop formula title, the related record's own formula title, andnullfor an empty lookup.packages/runtime/src/app-artifact-handlers.hook-title.test.ts(new) uses a realObjectQLthat dispatches throughtriggerHooks. It checks five things. Both forms answer, and an absent title isnull. The related read goes once through the hook context's ownapi. Writes land on the engine's context. That context never gains atitlekey. The app's declaration keeps its own function.a14895b71, the final head, which is a merge oforigin/main51ad8e4fb:pnpm --filter @objectstack/runtime typecheck: exit 0.pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2: 420 files passed; 6608 tests passed, 19 skipped.pnpm --filter @objectstack/verify typecheck: exit 0.pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2: 42 files passed; 364 tests passed.pnpm exec eslint --no-inline-configover the four changed source and test files: exit 0, no findings.node scripts/pm/dispatch-gates.mjs --commands, plus the 10 declared wide-population families whose population coverspackages/runtimeandpackages/verify, all exited 0.--ranreconciliation: 63 derived, 63 run, 0 NOT-MEASURED.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, nodist/). After a fullturbo run build, it exited 0.Acceptance notes
handlerthat names a function. Hook ctx.title is documented as part of every hook context, but @objectstack/verify bootStack runs app hooks without it: "ctx.title is not a function" under the test harness, while objectstack dev serves it #24289 remains open for it. This PR covers a hook whosehandleris a function. A hook whosehandlerNAMES a function also runs in-process: that is the deprecated string form, and also whatobjectstack buildships for a callable it could not lower to a body. Such a hook is still not givenctx.title, because the engine's binder looks the function up by name afterbindAppArtifactHandlershas run. Measured at7bd31c8e5in a one-off run that was not committed:typeof ctx.titlewas'undefined'in two cases. One was the artifact door with aruntimeModuleand a stringhandler, which is the loaderos start --artifactuses. The other was a source boot withfunctionsandhandler: 'name'. The same function as an inline handler answered. The measured route is onebindHooksToEngineoption (packages/objectql/src/hook-binder.ts), plus theIObjectQLEngine.bindHooksoptions type (packages/spec/src/contracts/objectql-engine.ts, about line 396) and theObjectQL.bindHooksoptions (packages/objectql/src/engine.ts, about line 4849). That makes it a cross-lane slice, engine and spec.HookContextdeclares notitle.HookContextSchema(packages/spec/src/data/hook.zod.ts) has notitlemember. Undertsc --strict, a handler annotated(ctx: HookContext)that callsctx.title()gets TS2339, while an unannotateddefineStackinline handler compiles. Declaring the member is a published-type change in the spec lane. It is sequenced after the slice above, so the declaration does not run ahead of what the runtime delivers.skills/objectstack-data/references/data-hooks.mdis not edited. It already describes abodycontext as a subset of the in-process one, and this change makes that true forctx.titleon a function handler.Written in Claude Code session
session_01SYEJqDL3NXxrJRBVqaMu9T.