Repository navigation
record-change flow trigger 在 predicate 批量写上:previous 不绑定、record 只是裸 payload —— 官方文档与 showcase 正在教的 transition 起始条件在批量写上不成立 #4862
Description
Activity
分诊:过升级门槛,挂
needs-user-decision上交维护者 —— 与 #4800 联合拍板正文的 A/B/C 三方向是真正的产品语义分歧,且 A(按行触发)与 #4800(批量写上 hook 是否按行)同源 —— 分开定必然产出两套规则。按决策分析两轴补充:
- 项目长远合理性:A 是唯一让「官方文档与 showcase 正在教的 transition 条件」在所有写路径上语义成立的方向;B 一致但把声明错误变成写入失败(hook 的
condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 已为同类爆炸半径付过学费);C 保留 declared ≠ enforced,与本仓路线相悖。 - 防 AI 写元数据犯错:A 落地后声明即兑现,最优;B 次之(契约收紧、错误响亮,AI 写出引用 previous 的 flow 会在第一次批量写上被立刻打回);C 最差 —— 静默跳过恰是 AI 批量犯错被掩盖的温床,即便加了 error 日志也只是把坑标了个牌子。
建议:A,与 #4800 同一次拍板;若 A 的成本(批量写多一次 read + N 次评估)不可接受,取 B,不取 C。
拍板前本车道不派发;showcase 那 10 条受影响 flow 在拍板后随实现单一并处理。
Generated by Claude Code
- 项目长远合理性:A 是唯一让「官方文档与 showcase 正在教的 transition 条件」在所有写路径上语义成立的方向;B 一致但把声明错误变成写入失败(hook 的
维护者已裁(2026-08-04):取 A(按行触发),与 #4800 同一契约、同一实现单交付
trigger 侧不单独动 —— 修复随「批量写按行语义」实现单(见 #4800 的裁定评论与新立实现单)一并落地;showcase 受影响的 10 条 flow 届时一并验证。摘
needs-user-decision,挂 Blocked-by 到实现单,拍板已毕。
Generated by Claude Code
PM 分诊(p0 #5088 集中处理的同一族,第 1 轮):标记
needs-user-decision,建议与 #4800 同批拍板;本条不阻塞 #5088。1. 与 #5088 的边界
今天开的 p0 #5088(
updateMany对不存在的 id 报INTERNAL_ERROR而非RECORD_NOT_FOUND)与本条同属「批量写上下文残缺」这一族,但不是同一条路径:- 本条:predicate 批量写(
multi: true,无 id)—— 引擎批量分支result = await driver.updateMany(...)返回受影响行数,record退化成裸 payload、previous恒undefined。正文第 1–5 点的实测 PM 已在origin/main复核,成立。 - data: updateMany runs hooks for a nonexistent id — the row fails INTERNAL_ERROR from a hook-condition abort instead of RECORD_NOT_FOUND #5088:
records: [{id, data}],每行自带 id —— 落点是packages/metadata-protocol/src/protocol.ts的三个 bulk 写面,缺的只是 data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435 那个逐行存在性探针。
因此 #5088 按「恢复既有不变量」直接派发,派发词明确禁止改动 predicate 批量分支的 hook / flow 语义 —— 本条的决定权范围不受影响。
顺带一条对本条有用的实测:#5088 排查时确认
protocol.ts上 #4435 的「按行诚实」只落在 5 个写面里的 2 个(updateData、deleteManyData),updateManyData与batchData的 update/delete 分支都还缺。与本条是同一个形状的又一例 —— 一个修复只落在提出者当时站着的那扇门上,其余同族入口原样留着。2. 为什么标
needs-user-decision正文明写「可选方向(未拍板,留给维护者)」,且 A/B/C 已按两条轴分析完整,PM 不再补。要拍板的是 flow 起始条件在批量写上的契约形状,属升级门槛第一条(产品语义 / 公开契约),PM 不代答。
需要维护者知道的两点权重输入:
- 这不是理论缺口 —— 文档与 showcase 正在教的写法恰好落在这一格:
content/docs/data-modeling/formulas.mdx、skills/objectstack-formula/SKILL.md§5 教的是previous.x != … && record.x == …,而examples/app-showcase里约 10 条 flow 的起始条件就是这个形状。一次批量置 done 时,这些 flow 要么不触发、要么按一条并不存在的「记录」触发一次,且没有任何东西会说出来。审计 / 通知类 flow 的缺失是不可见的缺失。 - 同一平台对同一物理限制给了两种相反处理:hook
condition侧现在响亮失败(hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 B1),flow trigger 侧依然静默。选 C(显式跳过 + 可观测)会把这个不一致固化下来,选 B 会统一到「响亮」,选 A 才是消除限制本身。
3. 与 #4800 必须同批
#4800 的 A 与本条的 A 同源(批量写按行触发)。分开拍板必然产出两套规则,这正是本条正文点名要避免的。已在 #4800 同步标记并交叉说明。
Generated by Claude Code
- 本条:predicate 批量写(
- added a commit that references this issue
on Sep 4, 2026
来源
#4775(hook condition fail loud)落地过程中,为核实 B1 错误文案能不能把「改用 record-change flow trigger」写成出路而做的实测。核实结论是不能,#4775 因此没有在错误信息里指这条路(维护者定的硬防线:不允许在修
declared ≠ enforced的 PR 里于错误信息中再造一个同类缺陷)。核实过程本身暴露出这条独立缺陷,按 PD #10 单独记录。与 #4800 相邻但不是同一件事:#4800 是「批量写上 hook 要不要按行触发」的设计卡;本条是 record-change flow trigger 这一层,在批量写上交付给 flow 起始条件的上下文本身就是残缺的。
事实(实测,非推断)
对一次 predicate 批量写(
multi: true,无id):previous。packages/objectql/src/engine.ts:4888的if (priorRecord) hookContext.previous = ...,而priorRecord仅在if (hookContext.input.id)分支内被 fetch;批量分支始终为null。__previous兜底也不覆盖批量。packages/plugins/plugin-audit/src/audit-writers.ts:425:if (!id) return; // bulk update/delete — too costly to snapshot every row here。packages/triggers/trigger-record-change/src/record-change-trigger.ts:291-293:const previous = (ctx.previous ...) ?? ((ctx as ...).__previous ?? undefined);→ 批量写上
previous恒为undefined。record在批量写上也不是行。 引擎批量分支result = await driver.updateMany(...)返回的是受影响行数(number)。buildContext的after && typeof after === 'object'因此为 false,record退化成inputDoc—— 即本次写入的裸 payload,而不是任何一行的状态。为什么这条要单独立单
这不是一个理论缺口 —— 官方文档与 showcase 正在教的写法,恰好就落在这一格上:
content/docs/data-modeling/formulas.mdx与skills/objectstack-formula/SKILL.md§5 教的 transition 写法是previous.x != ... && record.x == ...;examples/app-showcase/src/automation/flows/index.ts里至少 10 条 flow 的起始条件是status == "done" && previous.status != "done"这个形状(第 39/317/424/671/780/896/976/1064/1148/1463 行等)。对一次批量更新(例如把一批 task 一次性置为 done),这些 flow 的起始条件面对的是「
previous未绑定 +record只有 payload」:该触发的自动化要么不触发,要么按一条并不存在的『记录』触发一次,而且没有任何东西会说出来。审计/通知类 flow 的缺失是不可见的缺失。与 #4775 的对照值得注意:hook
condition在这一格现在会响亮地失败(B1 专门诊断);而 flow trigger 这一格依然是静默的。同一个平台对同一个物理限制给了两种相反的处理。可选方向(未拍板,留给维护者)
previous(或引用 payload 未设置的字段)时,批量写直接失败,文案与 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 的 B1 同源。一致性最好,但把 flow 的声明错误变成写入失败,爆炸半径与 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 承担过的是同一类。倾向 A,并与 #4800 一起定方向;若 A 成本过高,B 比 C 更符合本仓已经选定的「declared = enforced」路线。
关联
condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775(已实现,PR feat(objectql)!: hook 的 condition 求不出值时 fail loud —— 抛错并中断该次操作 (#4775) #4861)—— 其 B1 错误文案刻意不指向 flow trigger,依据即本条skills/objectstack-formula的绑定作用域表要补一行:#4775 之后「condition 求不出值 = 该次写入失败」 #4814(SKILL.md §5 绑定作用域表)