feat(spec)!: flow 节点 config 契约收紧 —— schemaless 那三个从来没有过键闸门(#4001 批 9) - #4925
Merged
Conversation
…4001 批 9) Fourteen strip sites across the three automation node-config files close — the first `automation/` wave of the 2026-08-03 "necessary-and-complete" ruling. `automation/` remaining-strip: 67 → 53 (authorable 41 → 27). - io-node-config: NotifyConfig, HttpConfig - builtin-node-config: the CRUD quartet, Screen (+ field item + its options), Map - schemaless-node-config: Script, Subflow, Decision, DecisionCondition The deliberately-open `FlowNodeSchema.config` SLOT is untouched (ADR-0018 keeps `node.type` open for plugin executors); what closes is the per-node-type contract inside it. For `script` / `subflow` / `decision` this is the FIRST unknown-key gate, not a second one: `registerFlow()`'s #4277 rejection derives its declared set from a descriptor `configSchema`, and the schemaless class publishes none — so that walk skips them by construction. Curation is measured, not assumed. Every guidance/alias entry was checked against a repo-wide AST scan of 630 real flow-node payloads before it was written, and the campaign's finding 7 decided the one hard case: on a `decision`, `config.condition` is NOT renamed to `conditions` (one edit away, and taking that advice produces the double-declaration #4414 was filed for) — the mechanism is named instead. On a decision BRANCH the same word does rename, to `expression`. ADR-0087: no conversion needed. 160 flow nodes across the three shipped example apps were parsed against the new contracts — 52 carry one — with 0 unknown-key rejections, and all three `objectstack validate` runs pass.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 16:52
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 3, 2026
批 9 (#4925) landed the same automation/ ledger section. Both waves deleted their own rows and decremented the section header by their own count, so git merged the ROWS cleanly and conflicted only on the header — while the authorable subtotal one line below, which conflicted with nothing, merged clean while being wrong on both branches. Header recomputed from the surviving rows (42 strip of 75, authorable 16 of 42) rather than resolved in favour of either side; check:strictness-ledger's header arithmetic is the arbiter and reads 45 open files / 299 strip sites. os-regen four-step run: merge -> checkout origin/main for every path in .gitattributes -> wholesale regenerate -> assert siblings survived. 批 9's three reference pages and #4909's bulk-action page are byte-identical to origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
This was referenced Aug 3, 2026
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 3, 2026
合并 main 后 `pnpm build` 红:#4925 之外的既有缺陷被这次改动踩到。 `{ handler, effect: 'writes' }` 这个声明形态无法通过 `objectstack build` —— CLI 把它降级成 `{ handler: 'sweepProjectHealth', effect: 'writes' }` (lowerCallables 自己的测试就是这么断言的),而 `FlowFunctionEntrySchema` 的三个联合分支只接受:裸 callable、handler 是 callable 的声明、裸字符串 ref,唯独没有「handler 已被降级为字符串的声明」。于是从源码加载全绿 (dev / validate / vitest),只有构建产物这条路失败——正是 #4343 为裸形态 修掉的那个不对称,晚了一个形态。已拆 #4976。 `packages/spec` 本轮零改动,所以 showcase 改用裸函数形态,并在原处留下 不要改回去的理由 + issue 链接。运行时没有损失:`effect` 只有 script 节点 的 `unmeasuredEffect` 一个消费者,job 这条路的 `collectBundleFunctions` 本来就只保留 handler。 守卫相应调整为「functions 条目必须是构建能承载的形态」,并注明 #4976 落地 后应当删除该守卫而不是绕过它。 复核(合并 main 后重跑):`pnpm build` 全仓 71/71 绿,showcase 构建产物 bundle 到 2 个 handler;typecheck / 84 tests / validate 绿;全新库冷启仍是 2 条无关告警,且 `[migration] new datastore attested at creation` 与 `[value-shape] this deployment has verified …— enforced` 均出现。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 3, 2026
Ledger conflict was expected and is the three-way `automation/` header the dispatch note warned about: 批 9 (#4925) landed on main while this branch was open, and both sides edit the same section. Resolved by keeping BOTH sides' row edits and recomputing the header from the merged rows rather than taking a side: 批 9 removed builtin-node-config (8) / schemaless-node-config (4) / io-node-config (2), and rewrote their triage notes 批 11 removed flow-function (1) / time-relative-trigger (1) / webhook (1), and dropped flow.zod.ts 7 → 1 header 67 → 44 strip of 75 (13+10+1+6+5+5+4, summed from the rows) authorable 41 → 18 (etl 7 + state-machine 6 + control-flow 5) `check:strictness-ledger` arbitrates that arithmetic and passes; the header was NOT copied from either side. os-regen four-step ran in full: merge → `git checkout origin/main --` every `merge=os-regen` path → `pnpm install --frozen-lockfile` + rebuild spec + `check:generated --fix` (regenerated `content/docs/references/**` only) → assert BOTH sides survive. Sibling entries verified present after the wholesale regeneration: 批 9's NotifyConfig / HttpConfig / Script node-config pages and #4922's `data/SortNode` are all in `authorable-surface.json` beside this branch's `automation/Webhook:_packageId` / `:protection`. Merged-tree verification (§10: main touched packages/spec, the same package as this diff, so the full suite was re-run rather than scoped): check:generated 8/8 · all 8 pure audits PASS (incl. strictness-ledger) @objectstack/spec 297 files / 7450 tests · tsc --noEmit clean Part of #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #4001
按 2026-08-03 11:42Z 维护者裁决派发的
automation/首批。三个文件、14 个 strip 站点归零,automation/剩余 strip 67 → 53(可授权 41 → 27),台账三行按反向闸门要求删除、表头算术已配平。⛔ 刻意不动:
FlowNodeSchema.config槽位本身。ADR-0018 让node.type对插件开放,插件 executor 自带configSchema,关掉槽位等于关掉那个扩展点。收紧的是槽位里面的每类型契约。一、这批里唯一不是「第二道门」的那三个
其余收紧批次的常见形态是:某处已经有守卫,schema 收紧是补上另一扇门。这批的前两个文件确实如此 ——
notify/http/ CRUD 四件套 /screen/map都是描述符带configSchema的内建,registerFlow()早就按描述符硬拒未声明键(#4277)。script/subflow/decision不是。validateNodeConfigKeys从描述符的configSchema推导「已声明集合」,而这三个类型按设计不发布configSchema,所以那个遍历按构造跳过它们(engine.ts 里写着这条豁免)。也就是说在这次收紧之前:这正是 #4001 第二类发现(「bespoke 守卫只守一扇门」)最纯粹的形态:守卫是为它作者面前那扇门写的,而它结构上覆盖不到的那一类,恰好就是没有第二扇门的那一类。所以对这两个节点(
script/subflow在 #4343 之后确实在执行期被 parse)这不是重复检查,是它们的第一道检查。decision仍是 export-only,收紧只绑定在tsc、published JSON Schema 和 objectui 对账三处。仍然收 —— 因为战役反复证明:一个留开的形状会长出一个测试、一张表单和一份 fixture 去断言那份开放,再关就是迁移而不是编辑。二、策展是量出来的,不是想出来的
每一条
guidance/aliases都先过了一次全仓 AST 普查:630 个真实 flow 节点载荷(examples / 平台应用 / 测试 / docs 代码块 / 转换 fixture),按节点类型收集config的顶层键与fields[]、conditions[]、options[]的子键,再和声明集合求差。没有任何真实载荷反驳的条目才留下。最难的一条,和战役的第 7 条发现同形。
decision的config.condition(单数)离conditions只有 1 个编辑距离,纯距离建议器会自信地提议改成复数。照做是更坏的结果:节点声明conditions同时出边带condition,就是「节点先选一支、这支的边再判一次」的双重声明,#4414 就是为这个开的。所以这条 suppress 掉改名,改说机制:分支活在出边上。而在 decision 的分支项上,同一个词的判决相反 —— 谓词槽真的叫
expression,且FlowEdgeSchema早就反向声明了expression → condition的别名。所以这里给condition → expression。一个词、两个面、两个方向都声明。其余成串的条目:
notifyto/subject/body/url/sourceflow-node-notify-config-aliasesobject/filters/fieldValuescfg.fields ?? cfg.fieldValues当年被明确否掉)recordIddelete_record上,一个「唯一的约束条件根本不被读」的删除节点就是 #3810,穿着一个读起来像约束的键。编辑距离够不到任何已声明键,不给处方就只能报个键名update_record/delete_recordoutputVariablescreenvisibleIf→visibleWhen、object→objectNamescreen不在flow-node-crud-object-alias的类型集合里,四个编辑距离也够不到,所以只有别名表能说这句话subflowflow、timeoutMsFlowNodeSchema.timeoutMsscriptfunctionName、inputinput的处方额外声明:单数在connector_action的connectorConfig.input上是规范的,别去"修"它http刻意没有策展表:普查没测到任何退役拼法或跨面近似,而两个可能的手滑(timeout→timeoutMs、header→headers)编辑距离本来就够得到 —— 这两条现在有测试钉着,一旦够不到了,注释就是错的、就欠一条条目。凭空造条目正是这场战役有一批一次性发出四条自信写错处方的原因。script的五个retiredKey墓碑不需要条目:墓碑本身更强(顺带把类型变成never),而strictObject已经把不可写的键排除出候选表(第 12 条发现)。有测试钉住这一点。三、ADR-0087:实测「否」
不是靠读代码下的结论。把三个示例 app 的 build 产物(
dist/objectstack.json,含 ADR-0031 region 递归)里每一个 flow 节点的 config 拿去按新契约 parse:先证红再信绿:往 app-todo 的产物里注入一个
negativeControlKey,探针立刻报红并打出完整拒绝信息,随后产物已还原。三个 app 的
objectstack validate全过(✓ Validation passed,showcase 29 flows)。四、仪器纪律 —— 这批让四把尺各红过一次
check:strictness-ledger反向闸门builtin-node-form-zod-ledger.test.ts(任务书点名要用作证据的那个)MapConfigSchema加一个probeOnlyDriftKeymap: read by the executor but absent from the designer form: expected ['probeOnlyDriftKey'] to deeply equal []✅MapConfigSchema换回z.objectDECISION_KEY_GUIDANCEexpected '…' to contain '#4414'✅authorable-surface.json把探针键写了进去(automation/MapConfig:probeOnlyDriftKey)。已git checkout复位后重新生成 —— 记在这里因为它是「变异测试的副作用会渗进生成物」这个具体的坑。五、objectui 跨仓
objectui 的
flow-node-config.spec-reconciliation测试比的是.shape的键集合,不是 parse,所以 strict 对它不可见。批 9 一个键都没动。 这个不变量现在有一条本仓测试钉着(SubflowConfigSchema/DecisionConfigSchema/DecisionConditionSchema的.shape精确键集),理由写在测试里:失败该落在做修改的那个仓库,而不是我们改不了的那个。另:objectui 的 decision 面板把单数
condition做成 legacy-gated(只渲染存量、不供新作者写),和本 PR 给condition的判决完全一致 —— 两边都说「它不在契约里」。六、散文也一起搬了门
三个文件的 module JSDoc 里都有一段「未知键是注册层的事,这里只 strip」。那句话现在不对了,已就地改写并说清两扇门各自的分工(注册那扇仍然是先到的、信息更全的那一扇 —— 它会按描述符 JSON Schema 递归下去,这才是它能抓到
fields[0].visibleIf的原因)。FLOW_NODE_UNKNOWN_KEY_GUIDANCE(engine 里注册门带的策展表)保留不动。按战役的结论:bespoke 守卫里值钱的从来不是检测(默认一翻就免费泛化),是那句话 —— 所以那句话被抄到新门上,而不是把旧门拆了。验证
合并
origin/main(8 commit,未触及packages/spec/src)后按 os-regen 四步协议同步:每个merge=os-regen路径先git checkout origin/main --,再从合并后的源整体重生成(不做文本合并)。spec-changes.json/authorable-surface.json/json-schema.manifest.json/api-surface{,-signatures}.json/protocol-upgrade-guide.md重生成后与origin/main逐字节相同 —— 这就是「兄弟 PR 的条目仍在」的断言。只有本批拥有的三张 reference 页有差异。上述验证全部是合并后重跑的读数。范围外发现(已按 PD #10 建单,未在本 PR 修)
renameConfigKey在规范键已存在时把被遮蔽的旧拼法原样留在存量元数据里(5 条转换的afterfixture 就是这么写的,且不发 notice)。收紧之前它在执行期被静默 strip;之后,对script/subflow这两个注册门够不到的类型,它会变成执行期硬拒。这是 ADR-0087 注册表的语义问题,两个方向都自洽,该由维护者定,不该由收紧批次顺手带过。本批的 guidance 已按「保持现状 + 带处方」的前提写好(每条都同时说清改名与删掉死孪生键)。recordId、decision 的condition、object(#4001 第一类发现的第七例) #4924 ——packages/spec/src/automation/flow.test.ts的 fixture 教了三种跑不通的形状:CRUD 的recordId、decision 的condition、object。它们断言的是刻意开放的FlowNodeSchema.config槽位,所以全绿、收紧后也仍然全绿 —— 不是会红的测试,是会被照抄的教材。第一类发现的第六、七例。讽刺的是,本批recordId那条 guidance 的经验依据,正是这些 fixture。相关
Fixes,campaign issue 跨批次保持 open)configSchemaand the keys its executor actually reads are still unreconciled —notifyhonourscfg.source, which no schema declares #4045 / The schemaless nodes' designer forms live only in objectui's hand-written table, and nothing reconciles them —scriptoffers three broken options and cannot author the one that works #4278 / 3b — wire the flow executors toparse()their config, and tighten the undeclared-key warning into an error #4277 /script的 config 契约要接入 #4277 的执行期 parse,先得有判别式(actionType)形态 #4343 / A decision node has three declared ways to route a branch and two of them do nothing — app-crm's convert-lead guard runs both branches #4414 / Console: screen-flow Submit never calls the resume endpoint — every screen flow is un-completable from the UI #3528 / Flow node filters silently blank date macros: the template engine consumes{…}before the query engine sees it #3810 / fix(automation): accept config.fieldValues as an alias for create/update_record fields #2419🤖 Generated with Claude Code
https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Generated by Claude Code