feat(lint): flow-node-write-unknown-field 覆盖 create_record(#4271) - #4374
Merged
Conversation
…4271) #4369 shipped the flow write-set gate on `update_record` alone and parked `create_record` in FLOW_WRITE_NODE_TYPES_DEFERRED with its reason — a gating rule earning its severity one measured surface at a time, recorded as data rather than left as silence. This measures the other half and moves it across. The INSERT path fails the same way, one notch harder. Same literal `config.fields` map, same `objectName` binding, same journey to the driver — the engine hands an undeclared key to `driver.create` verbatim, alongside the audit stamps. On SQLite/knex it becomes `table deal has no column named stagee` and the statement is rejected whole, so the correctly named fields in the same payload never land either. The extra harm is what does NOT exist afterwards: the row is never created, so every later node reading `{<node>.id}` from that node's `outputVariable` is working from a record that was never written. An `update_record` failure at least leaves the record intact. The message names that consequence on `create_record` and only there, instead of one sentence blurred to fit both. Nothing else moves: same rule id, same `error` severity, the same silent bails, and `runAs` still not consulted. Each skip is pinned on the create surface as well as the update one, so the two node types cannot drift apart. FLOW_WRITE_NODE_TYPES_DEFERRED is now empty and deliberately kept: the partition test derives the full `fields`-write-map set behaviourally from the spec's executor-written config schemas, so a node type that grows one later belongs to neither list and fails that test until someone classifies it. Deleting the empty array would turn that forced decision back into a default. Two non-members are now excluded on the shape of their failure rather than by omission — `get_record.fields` is a projection (a READ, where an unknown entry narrows the selection instead of breaking the statement) and `screen.defaults` is forwarded into the ScreenSpec the client renders, so an unknown key is a prefill the renderer ignores. That inert case is what this rule's severity is defined against; a test pins that an out-of-ledger node type stays silent. Verified against the repo's own apps: app-crm, app-todo and app-showcase all still validate clean with create_record covered — including crm's convert-lead flow, which creates an account and an opportunity before updating the lead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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.
背景
#4369 只把 flow write-set gate 落在
update_record上,把create_record连同理由一起停在FLOW_WRITE_NODE_TYPES_DEFERRED—— gating 规则一次只用一个实测过的面来赚它的严重级,而且把剩下那一半写成数据而不是留成沉默。本 PR 实测另一半,并把它移过来。
INSERT 的失败方式一样,而且更狠一档
同一张字面
config.fields、同一个objectName绑定、同一条到 driver 的路径:引擎把未声明的 key 连同审计戳原样交给driver.create。SQLite/knex 上变成:语句整条被拒,同 payload 里拼对的字段也没落库。多出来的伤害是事后什么都不存在:行根本没被创建,于是后面每个从该节点
outputVariable读{<node>.id}的节点,都在使用一条从未写入的记录。update_record失败至少还留着原记录。所以 message 只在
create_record上加上 "…and the record is never created at all",而不是把一句话糊成两边都能套。其余不动
同一个 rule id、同一个
error严重级、同一批静默 bail(模板化objectName、非字面fields、跨包对象、零字段对象、点号 key),runAs依然不看。每条 skip 现在在 create 面上也各有一条测试钉住,两个节点类型不会各走各的。空的 deferred 列表是刻意保留的
partition 测试是行为式地从 spec 里 executor 推导出的 config schema 求出"带
fields写 map 的节点类型"全集,所以将来某个类型长出写 map,它两边都不在,测试直接失败逼人分类。把这个空数组删掉,就是把这次强制决策退回成默认值。两个非成员改为按失败形状排除,而非省略
get_record.fields是 projection(z.array(z.string()))—— 一个读,未知项让选择变窄,不会打断语句;screen.defaults被 object-form screen 转发进客户端渲染的ScreenSpec,未知 key 是渲染器忽略的预填。后者正是这条规则的
error严重级用来对照定义的那个"跳过它、其余照常渲染"的惰性情形。新增一条测试钉住:ledger 之外的节点类型即使带fields也保持沉默。验证
@objectstack/lint42 文件 / 660 测试全绿(新增 6 条);typecheck、check:doc-authoring干净。文档无需改动:#4369 落下的那两处措辞说的是 "prefer a flow
update_recordnode … the field-existence check gates there too",本次扩大覆盖没有让其中任何一句变假。🤖 Generated with Claude Code