Repository navigation
Closed
Labels
bugSomething isn't workingSomething isn't workingneeds-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PRBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
Description
来源
#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):
- 引擎只在单 id 分支赋值
previous。packages/objectql/src/engine.ts:4888的if (priorRecord) hookContext.previous = ...,而priorRecord仅在if (hookContext.input.id)分支内被 fetch;批量分支始终为null。 - plugin-audit 的
__previous兜底也不覆盖批量。packages/plugins/plugin-audit/src/audit-writers.ts:425:if (!id) return; // bulk update/delete — too costly to snapshot every row here。 - flow trigger 读的正是这两个。
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,而不是任何一行的状态。- hook 只触发一次,所以即使匹配了 N 行,flow 也只被评估一次。
为什么这条要单独立单
这不是一个理论缺口 —— 官方文档与 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 这一格依然是静默的。同一个平台对同一个物理限制给了两种相反的处理。
可选方向(未拍板,留给维护者)
- A. 按行触发 —— flow trigger 在批量写上取回匹配行集,按行构造上下文。语义最正确,与 批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800 的 A 方向同源(两者应一起定,否则又是两套规则);代价是批量写多一次 read + N 次 flow 评估。
- B. 响亮拒绝 —— 起始条件引用
previous(或引用 payload 未设置的字段)时,批量写直接失败,文案与 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 的 B1 同源。一致性最好,但把 flow 的声明错误变成写入失败,爆炸半径与 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 承担过的是同一类。 - C. 显式跳过 + 可观测 —— 保持不触发,但按 error 级别上报并计数,至少让缺失可见。成本最低,但保留了「声明了却没兑现」。
倾向 A,并与 #4800 一起定方向;若 A 成本过高,B 比 C 更符合本仓已经选定的「declared = enforced」路线。
关联
- 起因:hook 的
condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775(已实现,PR feat(objectql)!: hook 的 condition 求不出值时 fail loud —— 抛错并中断该次操作 (#4775) #4861)—— 其 B1 错误文案刻意不指向 flow trigger,依据即本条 - 相邻:批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800(批量写 hook 按行触发的设计卡)
- 文档面:
skills/objectstack-formula的绑定作用域表要补一行:#4775 之后「condition 求不出值 = 该次写入失败」 #4814(SKILL.md §5 绑定作用域表)
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingneeds-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PRBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
Activity
claude commentedon Aug 4, 2026
分诊:过升级门槛,挂
needs-user-decision上交维护者 —— 与 #4800 联合拍板正文的 A/B/C 三方向是真正的产品语义分歧,且 A(按行触发)与 #4800(批量写上 hook 是否按行)同源 —— 分开定必然产出两套规则。按决策分析两轴补充:
condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 已为同类爆炸半径付过学费);C 保留 declared ≠ enforced,与本仓路线相悖。建议:A,与 #4800 同一次拍板;若 A 的成本(批量写多一次 read + N 次评估)不可接受,取 B,不取 C。
拍板前本车道不派发;showcase 那 10 条受影响 flow 在拍板后随实现单一并处理。
Generated by Claude Code
claude commentedon Aug 4, 2026
维护者已裁(2026-08-04):取 A(按行触发),与 #4800 同一契约、同一实现单交付
trigger 侧不单独动 —— 修复随「批量写按行语义」实现单(见 #4800 的裁定评论与新立实现单)一并落地;showcase 受影响的 10 条 flow 届时一并验证。摘
needs-user-decision,挂 Blocked-by 到实现单,拍板已毕。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
PM 分诊(p0 #5088 集中处理的同一族,第 1 轮):标记
needs-user-decision,建议与 #4800 同批拍板;本条不阻塞 #5088。1. 与 #5088 的边界
今天开的 p0 #5088(
updateMany对不存在的 id 报INTERNAL_ERROR而非RECORD_NOT_FOUND)与本条同属「批量写上下文残缺」这一族,但不是同一条路径:multi: true,无 id)—— 引擎批量分支result = await driver.updateMany(...)返回受影响行数,record退化成裸 payload、previous恒undefined。正文第 1–5 点的实测 PM 已在origin/main复核,成立。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 不代答。
需要维护者知道的两点权重输入:
content/docs/data-modeling/formulas.mdx、skills/objectstack-formula/SKILL.md§5 教的是previous.x != … && record.x == …,而examples/app-showcase里约 10 条 flow 的起始条件就是这个形状。一次批量置 done 时,这些 flow 要么不触发、要么按一条并不存在的「记录」触发一次,且没有任何东西会说出来。审计 / 通知类 flow 的缺失是不可见的缺失。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
fix(objectql): name the bulk-write limitation instead of blaming the …
fix(metadata-protocol): by-id bulk writes refuse a row that names no …
feat(objectql)!: 批量写按行语义 —— after 型 hook 按行触发,record-change trigger 按…
fix(objectql): bind `hookContext.previous` on a single-record delete (o…
test(#15356): re-check #4862 fact 4 directly — record is not the bare…