Repository navigation
Commit 29cbfcc
fix(lint): a flow condition's bare-name type verdict no longer refuses a name a screen field holds, and warns after a node that can bind any name (#30067)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBQYBMyxBQBC2UcSXZG86K9z)_
Fixes #30025
## 维护者速读
- **改了什么**:流程条件里的裸字段名类型判词不再误拒两类名字。`screen`
节点声明的字段名(恢复时按原名写入变量)不再当作触发对象的字段做类型判断;流程中有能写任意变量名的节点(`script`、`connector_action`、`wait`、`subflow`、`map`、无字段契约的
screen)时,裸名的类型不匹配降为 `type-mismatch-run-may-fail` 警告。`record.x`
的读取照旧报错。null-guard 判词的裸名集合同样剔除 screen 字段名。
- **为什么改**:记录触发流程里 screen 收集了数字 `title`,下游边写 `title >
5`,运行时读到的是收集值、条件能跑,发布门却按文本字段拒绝
`type-mismatch-run-fails`,属误拒。开放绑定节点之后同理:构建期证明不了裸名就是字段。
- **风险与回滚**:放宽发布门,不新增任何拒绝。降级只看流程自身的节点:入口(含运行时发布门的单次写入快照)只改变裸名和
`record.x`
读的是哪条记录,不改变节点是否重绑裸名,所以没有这类节点的记录触发流程,在每道门上的裸名拒绝都不变(#29926、#29959
的发布门钉照旧通过)。回滚即 revert,无数据迁移。
- **先看**:`packages/lint/src/validate-expressions.ts`(`flowBare`
的构建处与旁注)、`packages/formula/src/validate.ts`(`bareNamesUnproven` 与同源的
record
根回查)、`packages/lint/src/flow-variable-scope.ts`(`collectScreenFieldNames`)。
- **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run
src/publish-type-mismatch-fail-closed.test.ts`,预期 12 条全过:screen 字段持有的裸
`name > 5` 发布无判词;`script` 节点之后的裸 `name > 5` 发布带警告;两者后面的 `record.name >
5` 仍被拒;#29926/#29959 的原有拒绝不变。
- **需要你做的**:无,席位审阅并落地。
Clause-②: yes (widening) — the author-time gate (`os build`, `os
validate`, the package publish and the flow publish door) no longer
refuses, on a record-triggered flow (or a time-relative sweep of its own
object), a `type-mismatch-run-fails` on a BARE condition name that a
`screen` node's declared field holds (no verdict now), nor on a bare
name in a flow with a node that can bind a name no reader sees
(`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with
no input contract), which is now the `type-mismatch-run-may-fail`
warning. A `record.<field>` / `previous.<field>` read keeps its refusal,
and nothing new is refused. The null-guard verdict's bare names also
leave a screen's field names out. Added beside them: the optional
`bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`,
and `collectScreenFieldNames` in `@objectstack/lint`'s
flow-variable-scope module.
Metadata-protocol: none
Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx`
(the type-soundness row names the screen field and the open binder);
`skills/**` and the MCP skill text teach no bare flow-condition name a
screen or an open binder holds, so no skill or eval moves.
## What changes
Measured on `main` at `c23501b2`. Two kinds of name a bare
flow-condition read can hold instead of the trigger record's field were
still typed as the field, so a record-triggered flow was refused
`type-mismatch-run-fails` on a condition that runs:
| bare name | what the run binds (measured) | before | now |
|:--|:--|:--|:--|
| a `screen` node's declared field `name` | `applyResumeSignal`
(`service-automation/src/engine.ts`) lands the resume bag's `variables`
under their plain names, over the flattened field | typed as the field:
**error** `type-mismatch-run-fails` | not typed as the field (joins
`ExprSchemaHint.flowVariables`); no verdict |
| any bare name, in a flow with a node that can bind a name no reader
sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen
with no input contract; `flowCelRootScope`'s `CLOSED_BINDER_NODE_TYPES`)
| the node can write the live variable map or fold any resume bag |
**error** `type-mismatch-run-fails` | **warning**
`type-mismatch-run-may-fail` |
| `record.<field>` / `previous.<field>` in either flow | the field |
error | error (unchanged) |
**Screen fields (`@objectstack/lint`).** New
`collectScreenFieldNames(graphs)` in `flow-variable-scope.ts` reads
every `screen` node's `config.fields[].name` across every graph. The
flow walk unions it into the names it hands the type check
(`flowVariables`), and subtracts it from the null-guard twin's
`nullGuardBare`. `collectFlowVariableNames` is unchanged, so the #14089
shadowing warning and its other consumers are not moved.
**Open binders (`@objectstack/formula` + lint).** New optional
`ExprSchemaHint.bareNamesUnproven` (flattened scope only). In
`typeSoundnessIssue`, a `type-mismatch` on a bare name (no
`record`/`previous` root) under a declared outcome takes no outcome when
the key is set, so it is the existing run-may-fail warning. If a
`record.<field>` mismatch sits in the same source behind the bare one
(`name > 5 && record.title > 5`), `firstTypeMismatch` is asked again
with every bare name left `dyn`, and that record-root mismatch keeps the
error. Date verdicts are untouched.
**Which predicate (a measured departure from the order's literal
wording, agreed with the seat).** The order said to use
`rootScope.provable`, the null-guard twin's predicate. Measured: at the
runtime publish gate `rootScope` is built with
`perWriteSnapshotEntrance()`, which opens EVERY flow write, so the
literal reading would have dropped every bare-name refusal on every
record-triggered flow at the Studio/package publish door to a warning;
`publish-type-mismatch-fail-closed.test.ts` goes red on the #29926 start
pin and the #29959 decision pin (ablation A5 below). The flow walk now
asks the same judge, `flowCelRootScope(flow, graphs, objectName,
fieldIndex)`, with no entrance: only a NODE that can bind a name stands
the bare verdict down. An entrance (and the per-write snapshot, which
sees none) changes which record both the bare name and `record.<field>`
read, not whether a node rebinds the bare name, so it moves neither. The
null-guard twin keeps standing its bare names down on the whole
`rootScope`, as before. A code comment beside the call says so.
**Ledger.** `faultOutcomeOf`'s ledger docblock gains the screen field in
the variable row and a row for the open-binder stand-down;
`validate-null-guards.ts` point 2 names the screen fields and the shared
judge.
**Not done (optional item).** The engine's registration advisory pass
(`flow-registration-checks.ts`) still passes no `flowVariables`:
`@objectstack/service-automation` does not depend on `@objectstack/lint`
(where `collectFlowVariableNames` / `collectScreenFieldNames` live), and
adding that dependency is outside this card. Its line is a log, never a
refusal.
## Tests
- `packages/formula/src/validate.test.ts`: with `bareNamesUnproven` and
`run-fails`, bare `name > 5` / `is_active > 0` / `name * 2 > 1` → one
`type-mismatch-run-may-fail` warning; `record.name > 5`,
`previous.is_active > 0` and `name > 5 && record.title > 5` →
`type-mismatch-run-fails` naming the record read; controls: key absent →
the error; `flowVariables` still untypes; a `record`-scoped
`fail-closed` site ignores the key.
- `packages/lint/src/validate-expressions.test.ts`, new `describe` "a
bare name a screen or an open binder can hold (#30025)":
record-triggered flow, screen field `title: number` before a `title > 5`
edge and a `title > 5` decision branch → no type verdict, only the
`record.title > 5` branch refused; the same after a `script` node and
after an `object-form` screen → two warnings, one error; the null-guard
twin: nullable `budget` with a screen collecting `budget` → no nullable
refusal, control screen collecting `note` → refused. The #5017 receiver
meta-test tables the `flowBare` / `screenFields` plumbing receivers.
- `packages/objectql/src/publish-type-mismatch-fail-closed.test.ts`, new
top-level `describe` in the existing harness, through the real
`saveMetaItem` (draft) + `publishPackageDrafts` door: screen-held bare
`name > 5` publishes with no type advisory; bare `name > 5` after a
`script` node publishes with the run-may-fail warning; `record.name > 5`
after either node is refused. The existing #29926 / #29959 refusals in
the same file are the publish-door pins that the bare refusal still
holds without such a node.
Ablations (each restored, tree clean after):
| | put back | red |
|:--|:--|:--|
| A1 | screen fields out of `flowBare.variables` | lint "does not type a
bare name a screen field holds…" |
| A2 | `unproven: false` | lint "warns, does not refuse, a bare-name
mismatch after a node that can bind any name" |
| A3 | formula: no record-root re-ask (`outcome = undefined` always) |
formula "drops a bare-name mismatch…" (`name > 5 && record.title > 5`) |
| A4 | screen fields back in `nullGuardBare` | lint "leaves the
null-guard twin silent…" |
| A5 | literal `unproven: !rootScope.provable` | objectql #29926
"refuses the comparison in a record-triggered flow condition…" and
#29959 "refuses a `name > 5` decision branch…" |
| A6 | `unproven: false` at the publish door | objectql "publishes a
bare `name > 5` after a script node with the run-may-fail warning" |
## Verification
All at head `71da71df`, under `os-verify-lock` (shared box). Base
`c23501b2`; `origin/main` is 3 commits ahead, none on the touched files,
and `git merge-tree` reports no conflict.
```
build pnpm --filter '@objectstack/lint^...' --filter @objectstack/lint build exit 0
build pnpm --filter '@objectstack/objectql^...' build exit 0
typecheck formula, lint, objectql exit 0
formula test Test Files 64 passed (64) Tests 1639 passed (1639)
lint test Test Files 193 passed (193) Tests 7977 passed (7977)
objectql test Test Files 547 passed (547) Tests 10836 passed (10836)
mcp src/mcp-validate-expression.test.ts Tests 35 passed (35) (formula hint parity)
pnpm lint exit 0
```
`node scripts/pm/dispatch-gates.mjs --commands`: 89 derived families,
each run, `CMD :: exit N` recorded, reconciled with `--ran`: **89
derived, 88 run, 1 NOT-MEASURED, 0 UNRUN**. 88 exit 0 (three of them,
`check-plugin-teardown-shape --self-test`, `check:skill-examples` and
`check:lean-entry-closure`, first exited 3 on a missing prerequisite and
pass after `git fetch --unshallow` and building objectql /
client-react). NOT MEASURED: `check:dual-build-cjs-loads`, reason: it
reads every package's dist (70 missing) and needs a full `pnpm build`;
CI's run is the measurement.
## Acceptance notes
- Flow-scoped, like `collectFlowVariableNames`: a screen field name is
excluded at every condition of the flow, the start condition included,
although a start condition runs before any screen. That is a missed
verdict (the safe direction), the same trade #29959 made for node ids,
for the reason `collectFlowVariableNames`' docblock gives (one variable
map per run; no traversal-order verdicts).
- The #14089 shadowing warning (`warnShadowedFieldReads`) still names
declared variables and node ids only, not screen fields. Not judged
here.
- A bare `date - date + 1` (`date-arithmetic`) after an open binder is
still an error: the card covers the type-mismatch verdict, and that one
is not an outcome verdict.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzicfrt7Yr8FaVdDsXD
<!-- signed: domain:server#6 · os-support-ai ·
session_01WGufzicfrt7Yr8FaVdDsXD -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d79eecf commit 29cbfcc
9 files changed
Lines changed: 305 additions & 24 deletions
File tree
- .changeset
- content/docs/data-modeling
- packages
- formula/src
- lint/src
- objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | | - | |
| 245 | + | |
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
851 | 851 | | |
852 | 852 | | |
853 | 853 | | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
854 | 887 | | |
855 | 888 | | |
856 | 889 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
270 | 282 | | |
271 | 283 | | |
272 | 284 | | |
| |||
414 | 426 | | |
415 | 427 | | |
416 | 428 | | |
| 429 | + | |
417 | 430 | | |
418 | 431 | | |
419 | 432 | | |
420 | 433 | | |
421 | 434 | | |
422 | 435 | | |
423 | 436 | | |
424 | | - | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
425 | 452 | | |
426 | 453 | | |
427 | 454 | | |
| |||
472 | 499 | | |
473 | 500 | | |
474 | 501 | | |
475 | | - | |
| 502 | + | |
476 | 503 | | |
477 | 504 | | |
478 | | - | |
| 505 | + | |
479 | 506 | | |
480 | 507 | | |
481 | 508 | | |
| |||
1405 | 1432 | | |
1406 | 1433 | | |
1407 | 1434 | | |
1408 | | - | |
| 1435 | + | |
| 1436 | + | |
1409 | 1437 | | |
1410 | 1438 | | |
1411 | 1439 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
134 | 137 | | |
135 | 138 | | |
136 | 139 | | |
| |||
248 | 251 | | |
249 | 252 | | |
250 | 253 | | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
251 | 284 | | |
252 | 285 | | |
253 | 286 | | |
| |||
0 commit comments