Skip to content

feat(lint): flow-node-write-unknown-field 覆盖 create_record(#4271) - #4374

Merged
os-zhuang merged 1 commit into
mainfrom
claude/flow-create-record-write-lint
Jul 31, 2026
Merged

feat(lint): flow-node-write-unknown-field 覆盖 create_record(#4271)#4374
os-zhuang merged 1 commit into
mainfrom
claude/flow-create-record-write-lint

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

#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 上变成:

insert into `deal` (`created_at`, `id`, `name`, `stagee`, `updated_at`) values (…) returning *
  - table deal has no column named stagee

语句整条被拒,同 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/lint 42 文件 / 660 测试全绿(新增 6 条);typecheckcheck:doc-authoring 干净。
  • 三个 example 在 create_record 纳入覆盖后依然全部通过 —— 包括 crm 的 convert-lead 流(先建 account 和 opportunity,再更新 lead)。

文档无需改动:#4369 落下的那两处措辞说的是 "prefer a flow update_record node … the field-existence check gates there too",本次扩大覆盖没有让其中任何一句变假。

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 9:37am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant