fix(objectql): 批量写上引用 previous 的 hook 条件 —— 点名「当前版本限制」的专门诊断 + ADR-0058 按行语义补遗 (#5037) - #5057
Merged
Merged
Conversation
…author, and record the per-row contract (#5037) The rc-window stopgap half of the 2026-08-04 ruling on #4800 / #4862. A hook `condition` reading `previous` on a predicate (`multi: true`) write is unevaluable — the engine matches N rows, fires the hook once and binds no prior record — and since #4775 that rejects the write. #4861 already gave the case its own sentence, but that sentence predates the ruling and led with "rewrite the condition without `previous`", which is advice to silently turn a transition into a state test. The contract is now the opposite: on a bulk write, after hooks and record-change flow triggers evaluate and fire PER ROW. - `docs/adr/0058`: addendum recording the contract, today's measured behaviour, the stopgap and its expiry (#5038), and the consequences #5038 must price. - `hook-wrappers.ts`: the rejection states it is a CURRENT-VERSION limitation, cites the contract and #5038, leads with the single-record route, and prices the rewrite. `HookConditionError.limitation` (`bulk_write_previous_unbound` | `bulk_write_stored_state_unavailable`) is the machine-readable discriminator — deliberately not `code`, since ADR-0112 makes `error.code` a closed wire vocabulary and rest-server promotes a thrown error's `.code` onto the envelope. - "does this condition read `previous`" now comes from the parsed CEL AST (`collectCelRootIdentifiers`), computed once at wrap time, with the fault-text check as fallback — so the diagnosis no longer rides on cel-js's wording, and `record.previous_status` is correctly NOT a `previous` reference. - `scripts/adr-anchors.json`: anchors ADR-0058 + ADR-0112 to hook-wrappers.ts. Single-record writes, bulk writes whose conditions do not name `previous`, and the undeclared-key typo report are unchanged; fail loud takes no exception. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5037
维护者 2026-08-04 对 #4800 / #4862 的拍板里,本单是 rc 止血半步:契约(批量写按行求值、按行触发)记进 ADR,实现留给 #5038,rc 期把撞上限制时的报错从「像是作者写错了」改成「点名这是当前版本的限制」。
先说清楚现状:第 1 项已部分落在 #4861 里
派单文里描述的「批量写 + 引用
previous→ 抛泛化的No such key: previous」在origin/main上已不成立:PR #4861 在实现 #4775 的同时就带了predicateBulkWrite分支。实测(worktree 内跑现有用例)当前报错是:所以本 PR 不是「补一条没有的诊断」,而是把这条诊断对齐到 8-04 的裁定,并补上裁定要求而当时没有的三样东西:契约口径、机器可读判别位、ADR 记录。这一点写在这里,免得 review 时按派单文原样核对。
改了什么
1. 文案:从「换个写法」改成「这是当前版本的限制」
旧文案第一条出路是 rewrite the condition without
previous。在 8-04 的裁定之后这句话是错的建议:作者写的过渡条件是合法的、平台已声明要支持的形状;去掉previous会把「刚变成 done」悄悄变成「是 done」,于是本来只该记一次的审计,在每一行早就 done 的记录上都会触发 —— 一个静默的语义改变,正是 #4649/#4775 一路在清的那类东西。现在的文案:点名批量写与「没有单一前置记录」(保留),然后说明这是当前版本的限制而非契约、引用契约出处(ADR-0058 补遗、#4800/#4862)与退役条件(#5038),先给今天可用的出路(改单记录写入,同一条件原样成立),再把「删掉
previous」标价而不是推荐。原有的「record-change flow trigger 不是绕路」一句保留 —— #4862 是实测结论,它订的是同一批生命周期 hook,批量上同样拿不到previous。2. 判别位:
limitation,并且故意不叫codeHookConditionError新增limitation?: 'bulk_write_previous_unbound' | 'bulk_write_stored_state_unavailable'(类型HookConditionLimitation一并导出),与既有的reason/predicateBulkWrite同族,调用方判分支不必再匹配文案。没有叫
code,是因为契约在别处:ADR-0112 把error.code定为闭集(StandardErrorCode∪ERROR_CODE_LEDGER,都在packages/spec/src/api/),而rest-server.ts会把抛出错误的.code直接提升到响应封套上。在这里加一个.code,等于以副作用的方式凭空造出一个未注册的线上错误码 —— 正是这一族错误要消灭的declared ≠ enforced形状。真要上线,应当走 ledger 注册这条路(本轮spec/**零改动,不做)。3. 检测:走解析后的 CEL AST,不再依赖 cel-js 的措辞
「这条条件是否引用了
previous」改由collectCelRootIdentifiers(#4972 的构建闸门用的同一个工具)在 wrap 时算一次,旧的 fault 文本判断降为兜底。实测两条路今天答案一致,收益是这条面向作者的诊断不再挂在上游库的错误措辞上;顺带修掉一类会错判的形状:record.previous_status是record下的成员名,AST 给出的 root 只有record,不会被当成previous引用(按文本匹配就会)。工具已知的 caveat(comprehension 绑定变量会被算作 root)在这里是惰性的:该答案只在报错路径上被读,而[1,2].exists(previous, previous > 1)自绑变量、求值正常,永远走不到报错路径 —— 已就此加了用例。4. ADR-0058 补遗 + anchors
按 #4889 / PR #4972 的先例:只追加,不改旧文。补遗记的是契约本身(批量写上 after 型 hook 与 record-change flow trigger 按行求值、按行触发,与 validation 侧 #3106 同源)、今天引擎的实测行为、rc 期以本诊断止血及其退役条件(#5038),以及 #5038 实现时必须一并定的代价(hook 触发 N 次、
ctx.result形状、onError的按行语义、大批量上限)。scripts/adr-anchors.json把 ADR-0058 + ADR-0112 锚到hook-wrappers.ts。回归测试
新增
packages/objectql/src/hook-condition-bulk-previous.test.ts(17 例),覆盖派单要求的四项:previous→limitation === 'bulk_write_previous_unbound'、文案含批量限制/当前版本/ADR-0058/[17.x] 批量写按行语义实现:hook 按行触发 + record-change trigger 按行绑定 previous/record(#4800/#4862 拍板 A) #5038/单记录出路;经真实引擎engine.update(..., { multi: true })仍然写入失败(诊断不是豁免);delete 形状的批量写同样命中;previous绑定、条件成立则 handler 运行、不成立则安静跳过、引擎路径上更新照常落库 —— 完全不变;previous的批量写:条件为真照跑、为假照跳、未声明键仍报「拼错」且不提批量;声明了但本次没写的字段保留自己的bulk_write_stored_state_unavailable;which this object does not declare/which is not bound for this operation均不出现,原始 fault 仍作为事实保留在fault字段),外加 AST 路线的两条钉子(混合拼错时两半都报;record.previous_status不误判)。本地闸门(均在
flock /tmp/os-heavy-verify.lock下、限堆、限包):合并
origin/main(含 #5045 动了packages/spec/src/contracts/objectql-engine.ts)后重装、重建依赖包并重跑 typecheck + 全量 objectql 测试,仍为 114/1810 全绿。约束核对
packages/spec/**与生成物:零改动;packages/metadata-protocol/src/protocol.ts:零改动;content/docs/releases/:未触碰。packages/objectql/src/{hook-wrappers,index,core}.ts+ 新测试、docs/adr/0058-*.md(仅追加补遗)、scripts/adr-anchors.json、一个 patch 级 changeset。engine.ts未改 —— 批量分支的事实(input.id缺失 +options.multi)已足够判定,不需要动那条同日被 fix(objectql,lint): 服务端补上 parent 作用域 readonlyWhen 的绑定与执行 (#4889) #4972/fix(objectql): resolve theNOW()defaultValue token in the engine (#4597) #4993/fix(objectql): 区分「同一实例的幂等重入」与「真正的 driver 名字冲突」(#4773) #5007 改过的路径。已知的下游口径(留给 #5038,不在本单)
skills/objectstack-formula/SKILL.md§5 目前把「两条出路」写成 rewrite the condition withoutprevious或改单记录写入,并把「过渡条件只写在单记录写入上」讲成永久规则;#5038 的验收面已包含「文档与 formula skill 的 transition 写法口径随之收口」。本 PR 不动它:改 SKILL.md 会带出gen:skill-docs的生成物,与本轮「spec/生成物零改动」冲突。🤖 Generated with Claude Code
https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
Generated by Claude Code