Repository navigation
hook 的 condition 求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775
Description
Activity
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions维护者已拍板:方案 C
决定:按 hook 类别分方向 ——
before*求不出值 → fail closed(拒写);after*求不出值 → 以 error 级别响亮上报并走既有onError,但不额外阻断写入。A、B、D 均不采纳。理由(维护者选择,记录在案):保留「错误必须响亮」这条真正重要的性质,同时让失败留在它该在的职责边界内 —— 不让一个审计 hook 的声明错误去打挂一次与它无关的写入。
一并生效的两条约束(原分析中提出,随本决定确认):
- 报错文案必须点名 hook、点名求不出的 key,复用 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 的错误形状;
- 这是破坏性变更:changeset 标 major,并写清「存量靠静默跳过苟着的 hook 会开始报错」—— 这正是该被发现的东西。
实现时需一并写清楚的一点:after 型的「响亮但不阻断」与既有
onError(abort/continue)如何复合,必须在文档与测试里钉死,不要长出第三套语义。排期:
Blocked-by: objectstack-ai/objectstack#4770。修法 1(merged record total)必须先落地 —— C 的两个方向都是定义在「已声明字段总全的 record 仍然求不出值」之上的,在合并之前实现会把「字段没改到」误判成声明错误。#4770 的 PR 合入后本 issue 自动进入派发队列。标签:移除
needs-user-decision,加pm:queue。
Generated by Claude Code
- changed the title
[-][决策] hook 的 `condition` 求不出值时应该怎么办 —— 「一律吞成 false + warn」的替代语义[/-][+]hook 的 `condition` 求不出值时:before* fail closed / after* 响亮但不阻断(方案 C,已拍板)[/+]on Aug 3, 2026 - changed the title
[-]hook 的 `condition` 求不出值时:before* fail closed / after* 响亮但不阻断(方案 C,已拍板)[/-][+]hook 的 `condition` 求不出值时:before* fail closed / after* 响亮但不阻断(方案 C 已拍板;Blocked-by #4770)[/+]on Aug 3, 2026 xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions我决定选择 B. 全局 fail loud:求不出值 → 抛错、中断该次操作(与 #4649 对齐)
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions⚠️ 更正上一条:维护者最终选择 方案 B,不是 C上一条评论记录的「已拍板 C」作废。维护者的最终决定是:
B. 全局 fail loud:求不出值 → 抛错、中断该次操作(与 #4649 对齐)。
即:hook 的
condition在合并出总全 record 之后仍然求不出值时,抛错并中断该次操作,错误点名 hook 与出问题的 key —— 不区分before*/after*,与 validation 谓词的 fail-closed 完全同源。A、C、D 均不采纳。这意味着(实现时必须照此办理,不要退回 C 的分档):
before*与after*同一个方向。一个afterUpdate审计 hook 的条件写错了,会让那次写入失败 —— 这是明知并接受的代价(我在分析里把它作为 B 的代价提出过,维护者拍板承担)。理由站得住:一条规则一个答案,平台不多一条「要看 event 类别才知道失败方向」的隐性规则;声明错了就该在第一次跑到时炸,而不是留一条没人会去找的审计空洞。- 相应地,不要引入「响亮但不阻断」这一档,也不要复用
onError(abort/continue)来软化它 —— condition 求值失败发生在onError管辖的 handler 执行之前,把它接进onError会长出第三套语义,正是本 issue 要消灭的东西。onError继续只管 handler 自身抛错。
随决定一并生效的两条约束(不变):
- 错误必须点名 hook、点名求不出的 key,复用 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 的错误形状(
rule-validator.ts),两边文案风格保持一致; - 破坏性变更:changeset 标 major,并写清「存量靠静默跳过苟着的 hook 会开始让写入失败」。B 比 C 更需要这一条 —— 预期它会当场揪出若干条从没真正生效过的 hook 条件(Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 用同一招揪出过两条我们自己的示例规则),那是该被发现的东西,不是要绕过的噪音。落地时应主动扫一遍仓内(含
examples/)的 hook condition,把 B 会打挂的先修掉。
排期不变:
Blocked-by: objectstack-ai/objectstack#4770。#4770(merged record total)的 PR #4786 合入后本 issue 进入派发队列 —— B 的语义定义在「已声明字段总全的 record 仍然求不出值」之上,合并逻辑没落地就实现会把「字段没改到」误判成声明错误,那会是一场灾难性的误伤。
Generated by Claude Code
- changed the title
[-]hook 的 `condition` 求不出值时:before* fail closed / after* 响亮但不阻断(方案 C 已拍板;Blocked-by #4770)[/-][+]hook 的 `condition` 求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770)[/+]on Aug 3, 2026 xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions排队更新:本 issue 多一条前置 ——
Blocked-by: #4784#4784(hook condition 的 CEL 作用域)已认领并拍板方案 A:给作用域补上
previous,与 validation 谓词对齐。这给本 issue 加了一条硬前置:本 issue 的 fail loud 会把「hook 条件里写了
previous.x」从静默失效变成打挂那次写入。而previous.*正是两处已发布 skill 文档在教的写法(skills/objectstack-formula/SKILL.md§5 的ISCHANGED(x)→previous.x != record.x迁移表)。先上 fail loud、后补作用域,等于对着一批照官方文档写的 hook 条件当场开火 —— 那不是「揪出从没生效过的声明」,那是自造事故。顺序因此固定为:
#4786(#4770 修法 1,合并队列中) └─▶ #4784(previous 进作用域,方案 A) └─▶ #4775(本 issue,fail loud,方案 B)Blocked-by: objectstack-ai/objectstack#4770, objectstack-ai/objectstack#4784。前面记过的两条约束不变(错误点名 hook 与 key;changeset 标 major)。再补一条:落地前扫仓内(含
examples/)hook condition 的那一遍,要在 #4784 合入之后做 —— 那时previous.*已经合法,扫出来的才是真正写错的,而不是「暂时还不支持」的。
Generated by Claude Code
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions排队更新:再加一条前置 ——
Blocked-by: #4800#4784 的实现(PR #4799)过程中暴露出一格 #4784 的拍板意见没有覆盖的语义,已拆成 #4800,它必须在本 issue 之前有答案。
原因与上一条
Blocked-by: #4784同源,但更硬:previous在 predicate(multi: true)批量更新上不绑定 —— 引擎在批量分支只触发一次 hook,hookContext.previous从不赋值,一次写入匹配 N 行,没有单一前态可绑。今天这只是 warn + 跳过。本 issue 的 fail loud 落地后,只要某个对象上存在一条引用previous的 hook 条件,该对象的每一次批量更新都会写入失败,而且失败原因指向一个与这次写入无关的 hook。而 #4799 恰恰是在鼓励作者写
previous.*(文档教的就是它,showcase 的审计 hook 也改成了这个形状)。先上 fail loud、后定批量语义,等于亲手把这条路铺成事故。链条现为:
#4786 ✅ → #4799(#4784,previous 进作用域,复核通过待合) └─▶ #4800(批量上 previous 的语义:A 按行触发 / B 专门诊断 / C 跳过 —— 建议 A,B 为低成本过渡) └─▶ #4775(本 issue,fail loud)Blocked-by: objectstack-ai/objectstack#4770, objectstack-ai/objectstack#4784, objectstack-ai/objectstack#4800。若 #4800 选 B(保持不绑定 + 专门诊断),那条诊断可以与本 issue 同批实现 —— B 的全部内容就是在 fail loud 里为这一格出一条点名的错误,而不是默认的
No such key: previous。届时本 issue 的派发范围要包含它。若选 A,则 A 先落地,本 issue 再跟。
Generated by Claude Code
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions前置更新:#4800 已拍板 B1,本 issue 的阻塞解除一项、派发范围扩一块
#4800 的答案(维护者已拍板):批量写上
previous保持不绑定,fail loud 不开例外 —— 但这一格的错误必须是专门诊断,不是默认的No such key: previous。对本 issue 的影响:
Blocked-by中 批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800 一项视为已解。剩余前置:bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784(PR fix(objectql,skills): hookcondition的 CEL 作用域补上previous—— 过渡语义可写,与 validation 谓词对齐 (#4784) #4799,已 ACCEPT 待合)。- 派发范围新增一块(随 B 的主体一起实现):
- predicate 批量更新上引用
previous的条件 → 失败时出专门文案:点名 hook、说明「predicate 批量更新没有单一前置记录」、给出路(单记录写入 / record-change flow trigger)。 - 写「flow trigger」出路前必须核实它在批量写上真的按行拿到
previous;不通则只指单记录一条路,并把 flow trigger 批量语义单独立 issue。 skills/objectstack-formula/SKILL.md§5 绑定作用域表的「predicate 批量更新 → unbound」行,补「hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 后:求不出值 = 该次写入失败」。
- predicate 批量更新上引用
- 原有约束不变:错误点名 hook 与 key(复用 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 形状)、changeset major、不接
onError软化、落地前扫仓内(含examples/)存量 hook condition(在 fix(objectql,skills): hookcondition的 CEL 作用域补上previous—— 过渡语义可写,与 validation 谓词对齐 (#4784) #4799 合入后做,那时previous.*已合法)。
链条现为:
#4786 ✅ → #4799(待合,CI 收尾中)→ #4775(本 issue,B + B1,一次派发)#4799 合入即派发本 issue。
Generated by Claude Code
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorAuthorMore actions移交:本条归 @os-zhuang 主会话派发(车道裁定),且维护者要求进本次发版
维护者已裁定 PM 车道划分:本会话(
session_018iARDqtrhQgz6fVHDeDkbQ)收缩到packages/lint/skills/**/content/docs/**;运行时与服务面(含packages/objectql)归os-zhuang主会话session_015Br2xsJsczFsTR9bvbh2Ny。本条落点是packages/objectql+examples/,因此移交。登记在 #4604。移交时一切已就绪,接手方不需要再问维护者任何问题:
- 方向已拍板 —— 方案 B:求不出值 → 抛错并中断该次操作,
before*/after*同一个方向。A(维持 warn+false)、C(按类别分档)、D(加onConditionErrorspec 键)均已排除,理由与两轴分析在上文评论里。 - B1 已拍板(批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800):predicate 批量更新上
previous不绑定,fail loud 不开例外 —— 但这一格必须出专门诊断(点名 hook、说明「这是 predicate 批量更新,没有单一前置记录」、给出路),而不是默认的No such key: previous。 - 一条硬防线:「过渡语义请用 record-change flow trigger」这条出路写进错误文案之前,必须先核实 flow trigger 在批量写上真的按行拿到
previous。不通就只指「改用单记录写入」一条路,并把 flow trigger 的批量语义单独立单 —— 不允许在错误信息里再造一次 declared ≠ delivered。 - 其余约束:错误必须点名 hook 与出问题的 key(复用 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649
rule-validator.ts的错误形状,两边文案风格一致);changeset 标 major;不要把它接进onError(abort/continue)软化成「响亮但不阻断」—— condition 求值发生在 handler 之前,接进去会长出第三套语义;落地前扫一遍仓内(含examples/)的 hook condition,把 B 会打挂的先修掉。 - 前置:bug(objectql): hook 条件对着「只含本次更新字段」的残缺 record 求值,取不到的字段被吞成 false —— 审计 hook 静默不触发 #4770 ✅ 已合(
84b6e58);bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784 的 PR fix(objectql,skills): hookcondition的 CEL 作用域补上previous—— 过渡语义可写,与 validation 谓词对齐 (#4784) #4799 已复核通过、在合并队列中,合入即解锁。批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800 已拍板,不再是阻塞。 ⚠️ 维护者指定本条进本次发版 —— 优先级按此安排。这意味着第 4 条的「存量 hook condition 全仓扫描」要在发版切版前完成,预期会当场揪出若干条从没真正生效过的条件(Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 用同一招揪出过两条我们自己的示例规则)。那是该被发现的东西,不是要绕过的噪音。
本会话不认领、不派发本条,
pm:queue保留以便你的 backlog 扫描捞到。
Generated by Claude Code
- 方向已拍板 —— 方案 B:求不出值 → 抛错并中断该次操作,
2 remaining items
认领:PM 循环第 4 轮(补批)
会话:session_015Br2xsJsczFsTR9bvbh2Ny
分支:claude/issue-4775-hook-condition-fail-loud
Worktree:objectstack-issue-4775接手说明:本单由
xuyushun441-sys/session_018iARDqtrhQgz6fVHDeDkbQ按 #4604 的车道裁定正式移交本会话(落点packages/objectql+examples/,属运行时面)。方向已由维护者拍板,维护者要求进本次发版,优先级按此安排。已移除needs-user-decision标签。前置链已全部清空,派发前逐条核过:
#4770 → PR #4786 ✅ 已合(84b6e58) #4784 → PR #4799 ✅ 已合(f160ba4a4) #4800 → 已拍板(方案 B:不绑定 + 专门诊断,与本单同批实现)采纳方案 B(全局 fail loud),不是 C。 issue 上有一条记录 C 的评论已被作者更正作废,派发文里我明确写了「不要退回 C 的分档」以防 dev 读到那条过时评论。
带进派发的约束(原样引自移交方的整理):
before*与after*同一个方向;一个afterUpdate审计 hook 条件写错会让那次写入失败 —— 这是明知并接受的代价,不是要绕的;- 不要引入「响亮但不阻断」,不要复用
onError软化 —— condition 求值发生在onError管辖的 handler 执行之前,接进去会长出第三套语义; - 错误点名 hook、点名求不出的 key,复用 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649
rule-validator.ts的错误形状; - changeset 标 major,写清「存量靠静默跳过苟着的 hook 会开始让写入失败」;
- 落地前扫仓内(含
examples/)hook condition,把 B 会打挂的先修掉 —— 这一遍要在 bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784 已合的前提下做(现已满足),那时previous.*已合法,扫出来的才是真正写错的; - 批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800/B1 同批:predicate 批量更新上
previous不绑定,fail loud 不开例外,但必须出专门诊断(点名 hook、说明「批量更新没有单一前置记录」、给出路),而不是默认的No such key: previous。
我额外强调的一条硬防线(移交方提出,我完全同意并加重):
写「用 record-change flow trigger」这条出路之前,必须先核实它在批量写上真的按行拿到
previous。不通则只指单记录写入,并把 flow trigger 的批量语义单独立单。不允许在错误信息里再造一次 declared ≠ delivered。一个「修 declared ≠ enforced」的 PR,若在错误信息里指一条自己没验证过的出路,就是在同一个 PR 里制造下一个同类缺陷。
⛔ 不写
skills/**:该目录按 #4604 车道裁定归xuyushun441-sys。SKILL.md 需要补的那一行已立为 #4809(他们的车道,Blocked-by: #4775),dev 只需在报告里写出那一行该说什么。
Generated by Claude Code
更正认领注释末尾的编号:SKILL.md 的跟踪单是 #4814,不是我写的 #4809(我先写了编号才立单,拿到的是 4814)。
约束不变:本单的 dev 不写
skills/**(该目录归xuyushun441-sys车道),只需在报告里写出那一行文档该说什么;#4814 跟踪它由对侧补上。
Generated by Claude Code
- added 11 commits that reference this issue
on Aug 4, 2026
背景
#4770 的修法 2。修法 1(合并
ctx.previous⊕input.data,让条件求值对着一条对已声明字段总全的 record)已按 #4649 /50185a8的现成语义派发实现中,分支claude/issue-4770-hook-condition-merged-record。修法 1 消灭掉的是「字段存在但本次没改」这一类,也就是眼下 showcase 刷屏的那一类。它不会消灭「求不出值」本身 —— 修法 1 之后仍然会有求不出值的条件:条件里写了一个未声明的 key(拼错、路径写错、引用了个已被退役的字段),或者表达式在运行期因别的原因 fault。#4649 的 materialise 是故意只覆盖已声明字段的,好让拼错的 key 依然炸出来、依然可报告 —— 所以「炸出来之后怎么办」这个问题是被有意留下的,不是被顺手解决的。
现状(
packages/objectql/src/hook-wrappers.ts:86):求不出值 →logger.warn→return false→ hook 不触发。具体问题
「表达式求不出值」和「表达式求出 false」被压成了同一个结果,而这两件事对不同 hook 意味着相反方向的风险:
before*,语义是"满足条件就拦下")—— 吞成 false = 放行一次本该被拦的写入;after*,语义是"满足条件就留痕")—— 吞成 false = 漏记一条本该有的审计。一个 hook 声明了 condition,平台却在自己算不出这个 condition 时安静地当它是 false —— 这正是 declared ≠ enforced:声明摆在元数据里、Studio 里看得见,运行时什么都不兑现,而且只留一条默认日志级别下会被当噪音划过去的 warn。#4649 对 validation 谓词已经拒绝了这个形状(选了 fail closed:谓词算不出就拒写,并点名规则和出问题的 key)。hook 这条路径要不要、以及怎样跟上,是本 issue 要定夺的。
为什么不由 PM 代拍:这不是"恢复既有不变量"那一类(那类我直接派发了),它要在几种语义上互斥的方向里选一个,其中两个会改变写入的运行时行为、一个会给 spec 增加一个新的可声明键 —— 属于公开契约形状,AGENTS.md / ADR / 现有代码惯例都没有决定答案。
可选方案
A. 维持现状(求不出值 → warn + false)
不改。
B. 全局 fail loud:求不出值 → 抛错、中断该次操作(与 #4649 对齐)
点名 hook 与出问题的 key,写入被拒。
C. 按 hook 类别分方向(before 型 fail closed / after 型 fail loud 但不阻断)
before*求不出值 → 拒写;after*求不出值 → 以 error 级别上报(并按onError走既有的 abort/continue),不额外阻断写入。onError(abort/continue)怎么复用,需要写清楚,否则会长出第三套语义。D. 变成可声明的:给 hook 加一个
onConditionError: 'skip' | 'abort' | 'run',默认非skip把方向交给声明这个 hook 的人。
skip—— 于是 A 的静默行为原地复活,只不过这次它是被"声明"出来的,连 warn 的立场都没有了。宽容的消费端正是 AI 批量犯错被掩盖的温床。我的建议
C,并把 B 作为可接受的次选。
两条轴的判断:长远合理性上 B 最干净(一条规则一个答案),但它把审计 hook 的声明错误变成用户写入失败,爆炸半径超出了那个 hook 的职责边界 —— 这不是"温和一点",而是归因错误:一次写入失败会指向一个跟它无关的 hook。C 保留了"错误必须响亮"这条真正重要的性质(两条轴里防 AI 犯错那条要的是不静默,不是必阻断),同时让失败留在它该在的地方。两轴在 B 与 C 之间的冲突就是这一点,如实摆在这里由你拍板。
A 和 D 我认为都不应选:A 是 #4649 已判过死刑的形状,D 是把平台该负的责任外包给最容易写错的一方,并且给静默行为发了一张许可证。
无论选哪个,有两点建议一并定下来:
关联
claude/issue-4770-hook-condition-merged-record)50185a8(随 PR fix(objectql): 校验规则谓词求值失败时 fail closed,并把合并记录补全为声明形状 (#4649) #4761 落地)—— validation 谓词 fail closed + merged record totalhas(x)reads as a null guard and is not one — a publish-time lint should reject un-guarded nullable comparisons in CEL predicates #4763(has(x)读起来像 null guard 但不是,publish 期 lint 拒绝未加保护的 nullable 比较)—— 同属"CEL 谓词写错了该在哪一层被抓住"的问题面,若本 issue 选 B/C,两者的错误信息风格应保持一致