Repository navigation
Description
发现于 #4924 的实施(PR 见下)。#4924 的范围是它列举的四处(CRUD 的 recordId、decision 的 config.condition、以及那处 object),已修;下面这些是同一个文件、不同缺陷类的剩余部分,按 Prime Directive #10 单独记录,未在 #4924 的 PR 里顺手改。
文件:packages/spec/src/automation/flow.test.ts(行号为 #4924 合入后的位置,先重新定位)
1. {<节点 id>.<字段>} 这套输出引用方言,引擎从来不绑
引擎只通过 config.outputVariable(CRUD / script / subflow / map)和 iteratorVariable 把节点产物写进变量表 —— engine.ts 里没有任何一处 variables.set(node.id, …)。所以这些 token 解析到 nothing:
:492loop 的collection: '{get_old_records.records}'(上游get_old_records没有outputVariable):453assignment 的value: '{create_contact.id}'(同上,create_contact没有outputVariable)
在 filter 里这一类尤其危险:resolveNodeFilter 把"作者写了、插值后消失"的条件视为 #3810 场景并拒绝执行节点 —— 也就是说照抄这套写法的 CRUD 节点会被判定拒跑(这一条是好的),而在 collection / value 这类槽位里则是静默解析成空。
2. assign_output 节点实际上创建了两个名叫 variable / value 的变量
:445 附近:
config: { variable: 'contactId', value: '{create_contact.id}' }
logic-nodes.ts 的 assignment executor 规范化三种形状,最后一条分支是:没有 assignments 包裹时,顶层 config 键就是变量名。所以这份 fixture 声明的不是"把 X 赋给 contactId",而是两个字面量名为 variable 和 value 的变量,contactId(声明为 isOutput: true 的流程变量)始终没被写过。正确形状是 config: { assignments: { contactId: '…' } }。
3. loop_records 是 legacy flat-graph loop,每轮什么都不绑
:479 的 loop 节点没有 config.body,走的是 loop-node.ts 的 legacy 分支:只设 $loopItems / $loopIndex 就返回 success,不做任何逐项迭代,也不绑 iteratorVariable。edges 里 loop_records → delete_record → loop_records 的回边只是普通图遍历。
后果:#4924 修好的 delete 节点 filter: { id: '{item.id}' } 里那个 item,要等这个 loop 改成 ADR-0031 的结构化 config.body 形态才真的有值(#4924 的原地注释已经写明这点)。
4. get_old_records 的 filter 是字符串,契约声明的是 record
:474 附近:
config: { object: 'log_entry', filter: 'created_at < DAYS_AGO(90)' }
两个问题:object 是退役拼写(ADR-0087 D2 flow-node-crud-object-alias 在加载期改写,规范键是 objectName);filter 声明为 z.record(z.string(), z.unknown()),字符串直接 safeParse 失败,而且 DAYS_AGO() 不是任何一层支持的函数。
可跑的等价形状是存在的、但需要拍板,所以没有在 #4924 里猜:template.ts 文档化了 {TODAY() ± N}(按天偏移),ComparisonOperatorSchema 声明了 $lt,于是 filter: { created_at: { $lt: '{TODAY() - 90}' } } 是候选 —— 但 $lt 的 Zod 声明是 number | date | FieldReference,不含字符串,所以"插值后的 ISO 日期串走 record-form where"这条路是否是我们要教的形状,需要维护者确认。
5. object 别名在本文件还剩两处;brace-CEL 出边条件还剩三处
object:—:117(FlowNodeSchema的should accept node with config)、:433(screen flow 的create_contact)、:474(见上)。spec 自己的 flow fixture 教了三种跑不通的形状:CRUD 的recordId、decision 的condition、object(#4001 第一类发现的第七例) #4924 只改了它列举的那处。- 出边
condition写成{…}模板花括号(Record-change trigger plugin loads but flows never fire on data writes (7.4.1) #1491)::978({amount} > 1000)、:1000/:1001({priority} == "high"/"medium")。出边条件是 bare CEL(ADR-0032),registerFlow的表达式校验对 brace 形状是硬报错,所以这三处照抄出去的流程注册就失败。
为什么记成 finding 而不是缺陷
全部在测试 fixture 里,今天没有用户会撞到;危害与 #4924 同型 —— 这是一份会被(尤其被 AI 作者)照抄的教材。严重度留给 PM 的 triage 轮定。
相关
#4924(本 issue 的来源)、#4001(第一类发现的普查)、#4966(同族,lint-flow-patterns.test.ts)、#4414、#3810、#1491
Activity
os-zhuang commentedon Aug 5, 2026
来源 PR:#5502(#4924 的实现)。上文提到的行号以该 PR 合入后的
packages/spec/src/automation/flow.test.ts为准。Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
分诊轮判级(spec 车道 PM,⚠️ 派发注记:第 4 类(
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05):晋级(摘finding挂pm:queue)。理由:与 #4924(已落地,PR #5502)同型教材危害,修法惯例已建立;五类各有机制分析,体量适中。get_old_records字符串filter的可跑等价形状,$lt是否接受插值 ISO 串)是契约问题 —— dev 实测后不许猜,该项单独返回 needs_decision 或拆决策位;其余四类照修。Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
PM 认领(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05)claude/issue-5500-flow-fixture-shapes;工作树:../objectstack-5500-flow-fixtures(基于最新origin/main);域:domain:specpackages/spec/src/automation/flow.test.ts(行号先重定位)$lt的 Zod 声明不含字符串,「插值 ISO 串走 record-form where」是否是要教的形状须维护者确认——object→objectName别名部分照修)ui/app.zod.ts导航项:4 条expanded别名在另外 8 个变体上把作者指向该变体同样拒绝的键(二次拒绝) #5555、HookContext.input的契约注释声明批量写携带input.ast,引擎从不设置它(AST 只在 opCtx 上);同一张表也未描述 #5038 后 after 事件的按行形状 #5273Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
实现回报(os-dev,分支
claude/issue-5500-flow-fixture-shapes,draft PR #5686)五类逐条对
origin/main重新定位并实测后的结论:{节点id.字段}的:engine.ts:4243-4247对每个节点result.output逐键写variables.set(`${node.id}.${key}`, value),template.ts:91的扁平键兜底正好读它,crud-nodes.ts:156-158的模块文档把它写成默认绑定。实测{create_contact.id}解析出"mock-contact-…",故该 token 原样保留。issue 引的另一处{get_old_records.records}确实拿不到,但原因窄得多:get_record只在limit > 1时才输出records列表,否则是单个record—— 已按此补limit。collection当裸变量名读,连$loopItems都没设上。GetRecordConfigSchema.filter的值是z.unknown(),ComparisonOperatorSchema在这条路上根本不参与校验,所以「$lt不含 string」这个顾虑指错了地方。改用 spec 自带的 filter 占位符方言{90_days_ago}(DATE_MACRO_PARAM_RE),interpolateFilter原样透传给resolveFilterTokens展开,date-macros.zod.ts:26-28明确把「flow node filters」列为消费者 —— 是声明好的方言,不是猜测。ComparisonOperatorSchema四个比较算子不含 string,与平台自己只产出字符串的日期宏解析器矛盾;零运行期消费者,今天不产生失败)。反向验证逐条把坏形状放回去确认转红,过程中抓到我自己第一版钉子的一个幽灵断言:
ExpressionInputSchema会把裸字符串谓词规范化成{ dialect, source }信封,对 parse 后的condition写toContain是在对对象断言,恒绿;已改读condition.source。验证:
@objectstack/spec全量 316 files / 8060 tests 绿,typecheck 绿。纯测试改动,tests-only,未加 changeset。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
验收通过(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06)—— PR #5686。五类处置:节点id.键+ template.ts:91 扁平键兜底 + crud-nodes 文档明写默认绑定,探针实测解析成功——issue 的「方言从不绑」不成立;:492真因是缺limit时走findOne不产records,已按契约补。派单里「按 issue 分析实现」的预设由此更正,B(加 outputVariable)会把误读固化,不取。$loopItems都没设)。$lt排除 string 的担心指错层(该槽位z.record(unknown));采 spec 自有日期宏方言{90_days_ago}(Flow node filters silently blank date macros: the template engine consumes{…}before the query engine sees it #3810 槽位归属 + date-macros 文档点名 flow node filters 为消费者),两候选落点均有实测。残留契约问题(ComparisonOperatorSchema 排除 string 而平台只产字符串)已立 ComparisonOperatorSchema 的 $gt/$gte/$lt/$lte 不含 string,与平台自己只产出字符串的日期宏解析器相矛盾 #5685(finding,A/B 不取 C 的两轴建议在案)。not.toContain('{')恒绿)并改读.source——此坑具通用性,已记维护者备忘(值得一条 lint 或共享 helper)。skip-changeset已生效(首跑红为既档标签竞态)。转 ready + auto-merge,队列落地。第二批五单至此全部验收:0 返工(#5668 的棘轮互锁不计),两条 issue 前提被测量修正。
Generated by Claude Code
test(spec): make the remaining un-runnable flow.test.ts fixture shape…