docs(spec): HookContext.input 契约表改成引擎真正构造的形状 (#5273) - #5668
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
范围外发现已按 PD #10 立项(均未指派,留给分诊轮判级),本 PR 不动:
两项立项前都做了关键词 + 文件路径检索,开放 issue 中无重复。 Generated by Claude Code |
|
CI 红分诊(spec 车道 PM, Generated by Claude Code |
…k-input-contract-prose
`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
追加:合入
|
| 命令 | 结果 |
|---|---|
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
Fixes #5273
按 issue 正文与分诊结论采「改注释,不改引擎」方向。把安全过滤后的 mutable AST 交给 hook 改写需要单独裁,本单不做。
前提核实(对合并 #5621 后的
origin/main)asthook.zod.ts:314-325)input: { ast }只出现在读路径engine.ts:4640/4773,都喂driver.find/findOne{ id, data?, options }5243(update)/5705(delete)opCtx.ast(#2982)hookContext.inputctx.input.ast消费者全仓
HookContext生产点恰好 5 处,全在engine.ts,外加 #5038 的buildPerRowAfterContexts。改了什么
input.ast」整句;批量写 before 事件的表行不再写ast。after*按匹配行派发,每行是单记录形状,input.id在那里是绑定的。input.id的精确措辞:批量 before 事件上id这个键存在、值为undefined(引擎用 shorthand 建{ id, … }),不是「没有 id」——因为'id' in input答 true。顺带说明:在before*里绑定它会把这次写改道到单条路径,那正是引擎的分发判据。*Many事件。同一张表里
insert: { doc: Record }也是假的 —— 引擎建的是{ data: row, options }。issue 没点它,但它就在我正在改写的这张表里,留着等于明知故留一条假行。已一并改正并加了 pin。packages/triggers/trigger-record-change里那条读input.doc的防御性兜底(其自身注释已写明doc只是 alias)是消费端 fallback,属 PD #12 债,另行立项,本 PR 不动。未做的事
hook-wrappers.ts的ast特判故意保留:派单说「实测安全才清」,实测结论是不安全——它在读路径上是活的。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)并运行:恰好三条红,全是旧契约声称、引擎从不兑现的键。正控同批为绿(
beforeFind确实带ast),所以「写路径没有 ast」是一次测量,不是空过。翻转后全绿。验证
--filter @objectstack/objectql test--filter @objectstack/spec testtypecheck--filter @objectstack/spec check:generatednode scripts/check-nul-bytes.mjseslint改动文件changeset:无
check:generated的check: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