发现于 #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: - `:492` loop 的 `collection: '{get_old_records.records}'`(上游 `get_old_records` 没有 `outputVariable`) - `:453` assignment 的 `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`(见上)。#4924 只改了它列举的那处。 - 出边 `condition` 写成 `{…}` 模板花括号(#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…