Repository navigation
Commit 98583cd
fix(formula): a bare field ref or mistyped root faults with Unknown variable; the refusal, template and docs stop saying it evaluates to null (#31117)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiP9RS1KjicEPyef7VNhFzFa)_
Fixes #31081
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:** `os validate` / `os build` 对裸字段引用(写了 `done` 而不是
`record.done`)和拼错根名的报错,不再说「表达式静默求值为 null」,改为说清真实机制:CEL 报 `Unknown
variable`,后果由所在位置决定(公式字段读到 null、error 级校验规则拒写、操作按钮的 `visible`
把按钮藏起来)。脚手架生成的 AGENTS.md 和四篇文档同步改正;`validating-metadata` 的「Fail
open」一行注明它说的是 Console 显示,服务端写入另有判定。
- **为什么改:** 旧说法只对公式字段成立。对校验规则、`requiredWhen`、选项 `visibleWhen`
来说,实际是拒绝写入;`readonlyWhen` 是锁住字段。作者照旧说法排查会找错方向。
- **风险与回滚:** 只改英文提示文字、注释和文档,错误码与参数不变,接受/拒绝的范围不变;一个 patch
changeset(`@objectstack/formula`、`create-objectstack`)。回滚即 revert。
- **先看:** `packages/formula/src/validate.ts`(`bare-reference` 与
`unbound-root`
两条消息);`content/docs/deployment/validating-metadata.mdx`(Fail open 行与其后的
submit 段);`packages/create-objectstack/src/templates/AGENTS.md`。
- **两分钟验证:** 在 `packages/formula` 下运行 `node -e
"import('@objectstack/formula').then(m=>console.log(m.validateExpression('predicate','!done',{scope:'record'}).errors[0].message))"`,应看到
`done is an unknown variable ... faults with Unknown variable: done ...
Write record.done`,不再出现 `silently evaluates to null`。
- **需要你做的:** 无,席位落地。
## What changes
A bare field name in a `record`-scoped expression (`done` where
`record.done` is meant) is never read as the field. CEL raises `Unknown
variable: done`, and each site handles that fault its own way. Each site
below was re-read against the code path that runs at it before
rewording.
| Site | Was | Now | What the runtime does (read on `origin/main`
5f00374) |
|:---|:---|:---|:---|
| `packages/formula/src/validate.ts` `bare-reference` refusal (printed
by `os validate`, `os build`, `validate_expression`) | "resolves to
nothing and the expression silently evaluates to null" | "is an unknown
variable and the expression faults with `Unknown variable: NAME` instead
of reading the field. The site's fault rule decides the outcome (a
formula field reads null, an `error` validation rule refuses the write,
an action's `visible` hides the action)." | One template serves every
record-scoped site (`expression-refusal.ts`: a code names one message
template), so the sentence names the mechanism and three outcomes that
are each true where they are named: formula fault reads null
(`reportFormulaFault`, objectql); an `error` validation rule is
fail-closed (`rule-validator.ts`, script/cross_field docblock); Console
`visible` fails closed (`DeclaredActionsBar.tsx`, `action:button`). |
| `validate.ts` `unbound-root` refusal | same phrase | "`NAME.…` is an
unknown variable, so the expression faults with `Unknown variable: NAME`
instead of reading it" | Same fault: `firstUndeclaredReference` keys on
cel-js's `Unknown variable` (`cel-engine.ts`). |
| `validate.ts` doc comments on `fieldTypes` and `scope` | "silently
evaluates to `null` / never fires" | fault, formula reads null, a
predicate is refused, locked, hidden or skipped by its site |
`faultOutcome` docblock in the same file already says this per outcome.
|
| `packages/formula/src/cel-engine.ts` `firstUndeclaredReference`
docblock | same | same as above | Comment only. |
| `packages/create-objectstack/src/templates/AGENTS.md` | "evaluates to
`null` and silently hides an action on every record" | the predicate
cannot be evaluated on any record: an action whose `visible` reads it is
hidden on every record, an `error` validation rule that reads it refuses
every write | The outcome for `visible` was right; the mechanism was
not. Ships in `dist/templates` (`tsup.config.ts` copies it), hence the
`create-objectstack` patch. |
| `content/docs/deployment/validating-metadata.mdx` code comment |
"resolves to null → the action is hidden on EVERY record" | unknown
variable, predicate faults on every record, the Console hides an action
whose `visible` faults | As above. |
| `validating-metadata.mdx` "Fail open" row | unqualified | "Fail open
in the Console's rendering", plus: the server judges `requiredWhen`,
`readonlyWhen` and an option's `visibleWhen` again on the write | Triage
6701911557: the row describes the Console. Server: `requiredWhen` and
option `visibleWhen` refuse the write (ADR-0137 D2,
`unevaluableOptionGateError`). |
| `validating-metadata.mdx` submit paragraph | "A broken `requiredWhen`
or `readonlyWhen` is refused by the server" | adds an option's
`visibleWhen`, and the one exception: a `readonlyWhen` reading a name
the write does not bind holds the lock (the change to that field is
dropped, the rest lands) | `isReadonlyWhenLocked` returns `true` on
`unknownVariableOf`, `stripReadonlyWhenFields` drops the field (#4889
carve-out); every other fault refuses. |
| `content/docs/getting-started/build-with-claude-code.mdx` code comment
and the quoted `os validate` transcript | old comment; old message
verbatim | new comment; new message verbatim | The transcript quotes the
refusal, so it follows it. |
| `content/docs/data-modeling/formulas.mdx` build-time check row | "a
bare `amount` resolves to nothing and evaluates to `null`" | unknown
variable, faults; a formula reads `null`, a predicate takes its site's
fault outcome | Added to the claim's surface (claim amendment
6706775171). |
| `content/docs/data-modeling/validation.mdx` callout | "resolves to
nothing and the rule silently never fires" | unknown variable; an
`error` rule refuses every write it judges, a `warning` / `info` rule is
skipped with a log line | `error` fail-closed; advisory fail-open
(`TYPE_MISMATCH_FAULT_ARMS['fail-open']` wording). Claim amendment
6706775171. |
| `packages/lint/scripts/check-doc-formula-expressions.mjs` header,
`packages/cli/test/authoring-rule-command-parity.test.ts` fixture
comment | "does not throw … silently evaluates to null"; "silently
evaluates to null" | never reads the field, faults, a formula reads
null; the `error`-by-default rule refuses every write | Comments only.
Claim amendment 6706775171. |
The `content/docs/**` pages are a `domain:devx` surface; they ride along
in this `domain:server` PR as triage 6701911557 allows.
Left as they are, because they are true where they stand: the
`type-mismatch` / `date-arithmetic` formula-field sentences ("the field
silently evaluates to null") and the `cel-engine.ts` ternary note, which
describe a formula field.
## Tests
- `packages/formula/src/expression-refusal-codes.test.ts`: the
`bare-reference` and `unbound-root` pins carry the new text. Codes and
params unchanged.
- Every other assertion on these refusals pins the code or the `bare
reference NAME` subject, which is unchanged, so none moved.
## Acceptance notes
- `ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts` (two
comments) and `skills/**` still carry the old phrase; out of this
claim's surface, listed in the report for the seat.
- `os validate` prints the message unwrapped; the transcript in
`build-with-claude-code.mdx` is hand-wrapped as the old one was.
`check:docs-transcript-drift` compares the rule count only.
## Verification
Head: `064a4d46f`
<details><summary>Local runs</summary>
All on head `064a4d46f`, worktree off `origin/main` `5f0037449`.
- `pnpm --filter @objectstack/formula test` :: exit 0 — Test Files 65
passed (65), Tests 1703 passed (1703); `pnpm --filter
@objectstack/formula typecheck` :: exit 0.
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` on
the seven files that assert these refusals
(`validate-expressions.test.ts`,
`validate-expressions.field-conditional-formatting.test.ts`, and the
five `runtime-gate.object-*-writes.test.ts` that pin a bare reference)
:: exit 0 — Test Files 7 passed (7), Tests 610 passed (610).
- `pnpm --filter create-objectstack test` :: exit 0 — Test Files 25
passed (25), Tests 374 passed (374).
- `eslint --no-inline-config` over the five changed code files :: exit
0.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 97 derived; 96 run with exit 0. Three first
answered exit 3 (prerequisite) and were re-run green after the
prerequisite was met: `check-plugin-teardown-shape.mjs --self-test`
(fetched the pinned fixture commit on this shallow clone),
`check:skill-examples` (built the `@objectstack/client-react` closure;
283 examples across 3 surfaces type-check), `check:lean-entry-closure`
(built the `@objectstack/objectql` closure). NOT MEASURED:
`check:dual-build-cjs-loads`, reason: it needs every package's `dist` (a
full `pnpm build`), and this diff changes no module shape; CI's `Lint &
Repo Gates` runs it.
- `dispatch-gates --ran` :: exit 0 — "97 derived famil(ies) accounted
for — 96 run, 1 NOT-MEASURED".
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>1 parent b5696b4 commit 98583cd
11 files changed
Lines changed: 72 additions & 32 deletions
File tree
- .changeset
- content/docs
- data-modeling
- deployment
- getting-started
- packages
- cli/test
- create-objectstack/src/templates
- formula/src
- lint/scripts
| 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 | |
|---|---|---|---|
| |||
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
242 | | - | |
| 242 | + | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
393 | | - | |
394 | | - | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
395 | 397 | | |
396 | 398 | | |
397 | 399 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
| |||
896 | 898 | | |
897 | 899 | | |
898 | 900 | | |
899 | | - | |
| 901 | + | |
900 | 902 | | |
901 | 903 | | |
902 | 904 | | |
903 | 905 | | |
904 | 906 | | |
905 | 907 | | |
906 | | - | |
907 | | - | |
908 | | - | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
909 | 914 | | |
910 | 915 | | |
911 | 916 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
303 | | - | |
| 303 | + | |
| 304 | + | |
304 | 305 | | |
305 | 306 | | |
306 | 307 | | |
| |||
315 | 316 | | |
316 | 317 | | |
317 | 318 | | |
318 | | - | |
319 | | - | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
320 | 324 | | |
321 | 325 | | |
322 | 326 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
| 100 | + | |
100 | 101 | | |
101 | 102 | | |
102 | 103 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
20 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
351 | 351 | | |
352 | 352 | | |
353 | 353 | | |
354 | | - | |
355 | | - | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
356 | 358 | | |
357 | 359 | | |
358 | 360 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
277 | | - | |
278 | | - | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
279 | 280 | | |
280 | 281 | | |
281 | 282 | | |
| |||
284 | 285 | | |
285 | 286 | | |
286 | 287 | | |
287 | | - | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
288 | 291 | | |
289 | 292 | | |
290 | 293 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
98 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
99 | 100 | | |
100 | 101 | | |
101 | 102 | | |
| |||
106 | 107 | | |
107 | 108 | | |
108 | 109 | | |
109 | | - | |
110 | | - | |
111 | | - | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
112 | 116 | | |
113 | 117 | | |
114 | 118 | | |
| |||
1247 | 1251 | | |
1248 | 1252 | | |
1249 | 1253 | | |
1250 | | - | |
1251 | | - | |
1252 | | - | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
1253 | 1257 | | |
1254 | 1258 | | |
1255 | 1259 | | |
| |||
1492 | 1496 | | |
1493 | 1497 | | |
1494 | 1498 | | |
1495 | | - | |
1496 | | - | |
| 1499 | + | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
1497 | 1504 | | |
1498 | 1505 | | |
1499 | 1506 | | |
| |||
0 commit comments