Skip to content

docs(spec): HookContext.input 契约表改成引擎真正构造的形状 (#5273) - #5668

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5273-hook-input-contract-prose
Aug 6, 2026
Merged

docs(spec): HookContext.input 契约表改成引擎真正构造的形状 (#5273)#5668
os-zhuang merged 3 commits into
mainfrom
claude/issue-5273-hook-input-contract-prose

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5273

按 issue 正文与分诊结论采「改注释,不改引擎」方向。把安全过滤后的 mutable AST 交给 hook 改写需要单独裁,本单不做。

前提核实(对合并 #5621 后的 origin/main)

断言 结果
契约表仍宣称批量写带 ast ✅ 成立(hook.zod.ts:314-325)
input: { ast } 只出现在读路径 ✅ 仅 engine.ts:4640 / 4773,都喂 driver.find/findOne
写路径构造 { id, data?, options } 5243(update)/ 5705(delete)
批量谓词走 opCtx.ast(#2982) ✅ 从不进 hookContext.input
零写路径 ctx.input.ast 消费者 ✅ grep 确认;读路径的两处消费者充当 #4865 正控

全仓 HookContext 生产点恰好 5 处,全在 engine.ts,外加 #5038buildPerRowAfterContexts

改了什么

  1. 删掉「the row-scoping predicate is carried in input.ast」整句;批量写 before 事件的表行不再写 ast
  2. 补上 [17.x] 批量写按行语义实现:hook 按行触发 + record-change trigger 按行绑定 previous/record(#4800/#4862 拍板 A) #5038 后的按行形状:批量写的 after* 按匹配行派发,每行是单记录形状,input.id 在那里绑定的。
  3. input.id 的精确措辞:批量 before 事件上 id 这个键存在、值为 undefined(引擎用 shorthand 建 { id, … }),不是「没有 id」——因为 'id' in input 答 true。顺带说明:在 before* 里绑定它会把这次写改道到单条路径,那正是引擎的分发判据。
  4. 保留仍然成立的两句:before 事件整批只触发一次;没有 *Many 事件。

⚠️ 超出 issue 正文的一处(请复核)

同一张表里 insert: { doc: Record } 也是假的 —— 引擎建的是 { data: row, options }。issue 没点它,但它就在我正在改写的这张表里,留着等于明知故留一条假行。已一并改正并加了 pin。

packages/triggers/trigger-record-change 里那条读 input.doc 的防御性兜底(其自身注释已写明 doc 只是 alias)是消费端 fallback,属 PD #12 债,另行立项,本 PR 不动。

未做的事

  • 不动引擎
  • hook-wrappers.tsast 特判故意保留:派单说「实测安全才清」,实测结论是不安全——它在读路径上是活的installFlatInput 对所有事件生效,beforeFind 的 input 确实带 ast;去掉特判后 input.ast = X 这种改写会被折进 data,反而弄坏一条真实能力。

pin 策略

走派单的第一选项,但落点必须在 objectql:spec 不能反向依赖 objectql(objectql 依赖 spec,spec 侧测试导入引擎会倒转依赖),所以事实 pin 在
packages/objectql/src/hook-input-shape-contract.test.ts,紧邻 #5038 自己的 bulk-write-per-row-hooks.test.ts。用 engine.registerHook原始上下文(不经 flat-input 代理),另有一组用 bindHooksToEngine 验声明式作者看到的答案一致——那句假话正是写给声明式作者的。

先证红(方向:预测为红,实测为红)

先按旧断言写(bulk before 有 ast、insert 有 doc)并运行:

FAIL  > `beforeUpdate` on a bulk write has no `ast` key
AssertionError: expected false to be true
 ❯ src/hook-input-shape-contract.test.ts:96:31
FAIL  > `beforeDelete` on a bulk write has no `ast` key
FAIL  > insert carries `data` — never `doc`

Test Files  1 failed | 119 passed (120)
     Tests  3 failed | 1931 passed (1934)

恰好三条红,全是旧契约声称、引擎从不兑现的键。正控同批为绿(beforeFind 确实带 ast),所以「写路径没有 ast」是一次测量,不是空过。翻转后全绿。

验证

命令 结果
--filter @objectstack/objectql test 1934 passed (120 files)
--filter @objectstack/spec test 8060 passed (316 files)
两包 typecheck 绿
--filter @objectstack/spec check:generated 10/10 up to date
node scripts/check-nul-bytes.mjs OK(另做控制字节自扫,干净)
eslint 改动文件 0

changeset:无

check:generatedcheck:docs content/docs/references/**含本次改动的 dist 上跑绿,说明这段 TSDoc 不流入生成参考页(content/docs/references/data/hook.mdx 的 input 行取自 .describe('Mutable input parameters'),本次未动)。故不触发 #5550 那条「生成文档移动就取 patch changeset」的前置条件;纯注释订正 + 测试,按 AGENTS.md「纯 bug 修复不需要 changeset」。如维护者认为「契约散文订正」应进发布说明,我补一个 patch changeset。


Generated by Claude Code

The `HookContext.input` table named three keys no producer sets:
`ast` on bulk update AND bulk delete, and `doc` on insert. `input` is
`z.record(z.string(), z.unknown())` — an open shape — so Zod validated
none of it and the prose was the only contract an author could read.

- Bulk writes carry no `ast`. The row-scoping predicate lives on the
  engine-internal `OperationContext.ast` (#2982) so middleware-composed
  filters bind the driver call where no handler can widen them. Deleted
  the "the row-scoping predicate is carried in `input.ast`" sentence.
- `input.id` on a bulk before-event is present but `undefined` (the
  engine builds `{ id, … }` with shorthand), not absent — documented as
  such, since `'id' in input` answers true.
- Documented the post-#5038 per-row after-event shape: `after*` on a
  bulk write dispatches once per matched row on a single-record-shaped
  context, so `input.id` IS bound there.
- insert builds `{ data }`, not `{ doc }`.

Kept: before-events still fire once per batch, and there is no `*Many`
event.

No engine change. The `ast` special-case in `hook-wrappers.ts`
deliberately stays — it is live on the READ path, where `input.ast` is
real and a handler may rewrite it.

Pinned in `packages/objectql/src/hook-input-shape-contract.test.ts`:
spec cannot execute a dispatch (objectql depends on spec, so a
spec-side test would invert the dependency), so the facts are asserted
next to the engine that produces them. `beforeFind` is the positive
control (#4865) — it really does carry `ast`, so "no ast on writes" is
a measurement rather than a vacuous pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 12:04am

Request Review

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Contributor Author

范围外发现已按 PD #10 立项(均未指派,留给分诊轮判级),本 PR 不动:

两项立项前都做了关键词 + 文件路径检索,开放 issue 中无重复。


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI 红分诊(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D):ESLint job 挂在 check:query-options-erasure 棘轮(267→268),同日语义互锁——该门禁由 PR #5600(#4918)在本分支切出后落地,dev 本地不可见;增量为本 PR 新测试文件的一处未类型化 engine 选项调用点。已回派原 dev 按门禁自身处方修复(类型化或 as unknown as EngineQueryOptions),推送后重跑。非质量问题,不计返工轮。


Generated by Claude Code

`check:query-options-erasure`(PR #5600 / #4918,在本分支切出之后落地)
把测试面计入只增不减的棘轮。本分支新增的正控里
`engine.find('task', {} as any)` 是其中一处擦除,使测试面 267 → 268
而红。

该调用点是**合约内**形状——`find(object, query?: EngineQueryOptions)`
的空查询——不是「断言引擎拒绝未知选项」那类刻意越契约的输入,所以按
门禁处方的第 1 条直接给它正确类型(这里等于去掉断言,签名本就能推
断),而不是写 `as unknown as EngineQueryOptions`。

基线文件未动:抬高天花板是「reviewed edit, not a remedy」,这里修的是
站点本身。

验证(合并 origin/main 后):
- node scripts/check-query-options-erasure-ratchet.mjs → 267,at the
  ceiling,no files added
- @objectstack/objectql test → 1934 passed (120 files)
- @objectstack/spec + @objectstack/objectql typecheck → Done

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D

Copy link
Copy Markdown
Contributor Author

追加:合入 main + 过 #4918 query-options 棘轮(dc6bfbf7a)

互锁背景

check:query-options-erasure(PR #5600 / #4918,devx 车道)在本分支切出工作树之后落地,本地看不见,所以首轮 ESLint job 才红。判定是 test surface grew 267 → 268 site(s),增量来自本 PR 新增测试里的一处调用点。

定位

棘轮的计数不是普通 pnpm lint 能复现的:携带该规则的 config 块把测试 glob 放在 ignores 里,棘轮测量时才把它摘掉。所以直接 npx eslint 那个文件报 0 条,必须照 measuringConfig(drop) 的做法把 QUERY_OPTIONS_TEST_GLOBSignores 摘掉再跑,才定位到唯一一处:

packages/objectql/src/hook-input-shape-contract.test.ts:80:31

即正控里的 await engine.find('task', {} as any)

另用「移走该文件再跑棘轮」交叉验证:移走 → 267 绿(at the ceiling),放回 → 268 红,增量确系本 PR 独有。

修法(按门禁自身处方的第 1 条)

该调用点是合约内形状 —— find(object, query?: EngineQueryOptions) 的空查询,不是「断言引擎拒绝未知选项」那类刻意越契约的输入。故按语义取路线 1 给它正确类型(这里等于直接去掉断言,签名本就能推断),而不是路线 2 的 as unknown as EngineQueryOptions —— 后者是为越契约输入准备的,用在这里会把一处普通调用伪装成有意绕过。已在该行留注释说明为何不擦。

scripts/query-options-erasure-baseline.json 未改:门禁原文说抬高天花板是「a reviewed edit, not a remedy」,这里修的是站点本身。git diff 对该文件为空。

证红 → 证绿

# 修前
✗ query-options-erasure ratchet (1 problem(s)):
  • test surface grew 267 → 268 site(s).

# 修后
✓ query-options-erasure ratchet holds: 84 unswept non-test site(s) in 19 file(s), none new.
  test surface: 267 site(s) in 51 file(s) — at the ceiling
  baseline key set verified against ec6fad8: no files added.

合并 main 后的重跑

git merge origin/main(至 ec6fad868,干净),按 AGENTS.md §9 重装 + 重建依赖后重跑:

命令 结果
check-query-options-erasure-ratchet.mjs 267,at the ceiling,no files added
--filter @objectstack/objectql test 1934 passed (120 files)
--filter @objectstack/spec --filter @objectstack/objectql typecheck Done / Done
check-nul-bytes.mjs OK(5597 文件);另做控制字节自扫,干净

§10 交叉检查:incoming 提交未触碰 hook.zod.ts / engine.ts / hook-wrappers.ts(git log 23dba626e..ec6fad868 -- <三者> 为空),故本 PR 的事实与散文在合并后的树上依然成立。

另:首轮 Check Changeset 的红是竞态,已自愈

该 job 在 23:33:33 判红,而 Auto Label 在 23:33:48 才把 skip-changeset 贴上 —— 门禁跑在标签落地之前。重跑该 run 后 0 failed job。标签本身是这个 PR 的正确出路(门禁 route 2:releases nothing,docs/tests-only),与正文「无 changeset」的判断一致。


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 28ce594 Aug 6, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5273-hook-input-contract-prose branch August 6, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

protocol:data size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants