Skip to content

Commit cef3b8f

Browse files
docs(skills): a bare field ref in a predicate faults, it does not read null (#31028)
<!-- claude-projects-attribution: {"github_login":"huangyiirene"} --> _Requested by **Irene** · [project thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xRbTTefYV6fumir4Fo1ax66)_ Fixes #30893 Clause-②: no Metadata-protocol: none `dispatch-gates` mandates the contract-review model tier for `skills/**` (clause ①). The maintainer's standing order in this project (2026-11-03) waives it; this PR was developed below that tier, and the dispatching seat runs the contract review. ## 维护者速读 - 改了什么:三个技能(`objectstack-platform/references/operations.md`、`objectstack-data`、`objectstack-ui`)不再说「谓词里裸字段引用求值为 `null`、静默隐藏或永不触发」,改为按 main 的代码逐个站点描述它的故障表现。 - 为什么改:main 上裸引用 `done` 是 `Unknown variable` 故障:`error` 校验规则、`requiredWhen` 拒绝写入并点名规则;选项 `visibleWhen` 只拒绝选了该选项的写入(表单仍提供该选项);`readonlyWhen` 字段在表单上仍可编辑,保存显示成功,服务端丢弃改动并记 warn;公式读 `null` 并记 warn;控制台里动作 `visible` 隐藏、`disabled` 置灰、字段 `visibleWhen` 照常显示,均有 console.warn。 - 风险与回滚:只改技能说明文字,无代码、schema 或行为变化;发布 `@objectstack/skills` 一个 patch;回滚即 revert。 - Token 账:合入 main(`33bc8ae5`)后 data 6068→6078、ops 3108→3106、ui 3544→3533,上限行都不动;增长由同文件删除重复句付账(见下文 Token ratchet)。 - 先看:`skills/objectstack-ui/SKILL.md` L267-272 与 `skills/objectstack-data/SKILL.md` L450-457;与卡片描述不同的三处见 "Deviations from the card"。 - 需要你做的:`skills/**` 是 Tier H,需要 `os-zhuang` 或 `hotlong` 批准。 ## What changes - `skills/objectstack-platform/references/operations.md`, Verify your work: a bare field ref "faults, so an `error` validation rule refuses each write naming the rule and an action's `visible` hides it". The dashboard-binding half and the `os validate` / `os build` advice are unchanged. - `skills/objectstack-data/SKILL.md`, Verify your work: the bare ref faults; "an `error` validation rule or `requiredWhen` refuses the write naming it, an option `visibleWhen` only a write picking the option (forms still offer it); a `readonlyWhen` field stays editable and the save succeeds without its change (logged); a formula reads `null` (logged)". RLS / sharing predicates are no longer listed (see deviation 3). The form-side field `visibleWhen` is left to the ui skill, which states it. - `skills/objectstack-ui/SKILL.md`, Verify your work: the bare ref "faults on *every* record: the action hides (`visible`) or greys out (`disabled`), the field shows; the console logs the fault". The lead-in now reads "otherwise met only at runtime" (the predicate trap is no longer silent). - `.changeset/30893-bare-field-ref-symptom.md`: `'@objectstack/skills': patch`, with `Clause-②: no` and `Metadata-protocol: none`. No `skip-changeset`. - Evals: `grep` of `skills/objectstack-{platform,data,ui}/evals/*.json` for null / hide / hidden / never / bare / `record.done` / fail-open/closed finds no case grading this symptom; no eval changes. `skills/objectstack-formula/SKILL.md` is untouched, as ordered. ## Each claim and the code read on `main` | Claim | Site read | |:--|:--| | A bare `done` faults (`Unknown variable: done`), it does not read `null` | `packages/formula/src/stdlib.ts` `buildScope` binds `record` / `previous` / `input` and namespaces only, no field flattening; the "evaluates to `null`" wording is the #1928-era comment on `firstUndeclaredReference` in `packages/formula/src/cel-engine.ts` | | `error` validation rule: write refused, naming the rule; `warning` / `info`: logged, write proceeds | `packages/objectql/src/validation/rule-validator.ts` docblock "Fail-CLOSED for unevaluable predicates (#4649)", `evaluateValidationRules`; probe below | | `requiredWhen`: write refused, naming field and rule | same file, docblock "ADR-0137 D2 — a field-rule predicate that FAULTS refuses the submit"; probe below | | Option `visibleWhen`: only a write that picks the gated option is refused; forms keep offering the option | same file, `evaluateOptionVisibility` over `pickedGatedOptions` (only written fields, only picked values whose option carries `visibleWhen`); the docblock's D3 render side ("a faulting option predicate leaves the option offered"); probe below | | `readonlyWhen` with a bare ref: the form keeps the field editable, the save succeeds, the server drops the change with a warning | server: same file, `isReadonlyWhenLocked` (any `unknownVariableOf(res.error)` → `return true`) and `stripReadonlyWhenFields` ("ignoring incoming change"); `packages/objectql/src/cel-fault.ts` `unknownVariableOf`; probe below. Form: `ui/packages/core/src/evaluator/fieldRules.ts` `READONLY_WHEN_FAULTED = false` (ADR-0137 D3). Lint's own `readonlyWhen` fault text in `packages/lint/src/validate-expressions.ts` says the same: "the field looks writable, the save reports success, and the value silently never lands" | | Formula field: reads `null`, warning logged once per object and field | `packages/objectql/src/engine-top-level.ts` `applyFormulaPlan` (`rec[fp.name] = null; onFault?.(…)`); `packages/objectql/src/engine-readable-ids-permission-formula.ts` `reportFormulaFault` (`logger.warn`) | | Action `visible` in the console: fails CLOSED, the action hides, `console.warn` names it | `ui/packages/react/src/hooks/useExpression.ts` `useCondition` with `throwOnError` (warns once, returns `false`); callers `ui/packages/components/src/renderers/action/action-group.tsx` `useMemberVisible`, `action-menu.tsx` `useMenuActionVisible`, `ui/packages/app-shell/src/views/DeclaredActionsBar.tsx` `useDeclaredActionShown` (objectui#11212) | | Action `disabled`: fail-soft `true`, so the action greys out, with a console warning | `useCondition` without `throwOnError` → `ui/packages/core/src/evaluator/ExpressionEvaluator.ts` `evaluateCelCondition` fast path (`evalFieldPredicate(source, record, true, …)`); `ui/packages/core/src/evaluator/fieldRules.ts` `warnPredicateFailure` (`console.warn`) | | Field `visibleWhen` on a form: fails OPEN, the field shows, fault reported | `ui/packages/app-shell/src/providers/ExpressionProvider.tsx` `evaluateVisibility` (objectui#6443) via `isObjectFieldVisible`; `ui/packages/react/src/utils/visibilityDiagnostic.ts` `reportUnresolvableVisibilityPredicate` (`console.warn`); the render side of ADR-0137 D3 per the rule-validator docblock | | RLS / sharing: a bare identifier is the column | `packages/formula/src/cel-to-filter.ts` `compileCelToFilter` operand case `'id'` ("Bare identifier = a single record field (RLS convention)"); `record.region` and bare `region` lower to the same column | Probe: a throwaway vitest file in `packages/objectql/src/validation/` (deleted, not committed) against `main` printed, for `done == true` with `done` declared: - `script`, `severity: 'error'`: `ValidationError` "Validation rule 'r1' could not be evaluated (type: Unknown variable: done) — write rejected." - the same at `severity: 'warning'`: no throw; logged "write proceeded: the rule is advisory". - `requiredWhen`: `ValidationError` "Field 'note' requiredWhen could not be evaluated (type: Unknown variable: done) — write rejected." - option `visibleWhen`: `ValidationError` "Option 'b' of field 'stage' visibleWhen could not be evaluated (type: Unknown variable: done) — write rejected." - `readonlyWhen`: warned "treating the field as LOCKED" and "Field 'amt' is read-only (readonlyWhen) — ignoring incoming change"; the payload came back `{}`. ## Deviations from the card The order says main's code wins over the card's paraphrase; the seat accepted these before the PR. 1. **Action `visible` fails closed, not open.** The card says `evaluateVisibility` shows the control and reports. On `main` that function is the field-`visibleWhen` gate and the published export; the action renderers call `useCondition(…, { throwOnError: true })`, which hides the action and warns (objectui#11212). The skills say the action hides; only the field fails open. 2. **`readonlyWhen` with a bare ref locks, it does not refuse.** The card points at ADR-0137 D2 (refuse). A bare `done` raises `Unknown variable`, which `isReadonlyWhenLocked` takes as the #4889 unbound-root case and LOCKS: the change is ignored and logged. `@objectstack/lint`'s fault-outcome ledger (`validate-expressions.ts`) lists `readonlyWhen` as refusing; that holds for its other faults, not this one. 3. **RLS / sharing dropped from the data skill's list.** The old text named them as bare-ref traps. Their predicates are compiled to a filter by `compileCelToFilter`, where a bare name is the column, so a bare ref is not the symptom described there. ## Token ratchet Measured with `node scripts/check-skills-token-ratchet.mjs` on `main` at `33bc8ae5` and on this head (`main` merged in). No ratchet row moves; nothing was re-wrapped to make room. Before the merge, on `bcf8a37e0`, the data file read 6078 → 6076; #30995 has since cut it on `main`. | File | `main` | This head | Ceiling | |:--|--:|--:|--:| | `skills/objectstack-data/SKILL.md` | 6068 | 6078 | 6078 | | `skills/objectstack-platform/references/operations.md` | 3108 | 3106 | 3108 | | `skills/objectstack-ui/SKILL.md` | 3544 | 3533 | 3544 | Deletions that pay the growth, each with its surviving home: - `objectstack-data/SKILL.md`: "but it does **not** replace `os validate`" (after "run it too"). Home: the same sentence's "additionally checks", and `objectstack-platform/references/operations.md` Verify "(`os lint` is a *separate* style/convention pass and does not replace `os validate`.)". - `objectstack-ui/SKILL.md`: "Don't report a view/action/dashboard done until `os validate` passes." Home: the same section's opener "run the author-time gate before reporting done", and operations.md "Rule of thumb: never report a metadata change as done until `os validate` passes." - `objectstack-data/SKILL.md` (review S1/S2 commit): the Verify passage's "a form shows a `visibleWhen` field". Home: `objectstack-ui/SKILL.md` Verify, "the field shows; the console logs the fault", and this file's "Use `visibleWhen` to hide irrelevant fields in ObjectUI forms". - `objectstack-data/SKILL.md` (same commit): "from any non-system write" in the `readonly: true` bullet, which restated the sentence's own subject ("A non-system write (REST/UI, …) has the field **stripped**"). - `operations.md`: the intro's "(ADR-0021)" after the dashboard clause. Home: item 3 of the same section, "**Widget-binding integrity (ADR-0021)**". The intro also drops its "ObjectStack" lead word. **Serial.** PR #31019 also edits `skills/objectstack-platform/references/operations.md` (other lines: the `os dev` / `os validate` rows, "Both run the same gates:", the `os deploy` row) and raises that file's ratchet row. Whichever of the two lands second merges `main` and re-measures. ## Verification Head `a7da7616`: the edit commit `87e83df5`, a merge of `origin/main` at `33bc8ae5` (no conflict, no rebase), and one commit for the contract review's two should-fixes in `objectstack-data/SKILL.md` (S1 option `visibleWhen` scope, S2 `readonlyWhen` form side). `main` has moved 3 commits since, none touching `skills/**` or the ratchet script, so it was not merged again. <details><summary>Local gates on a7da761 (this head): 46 derived, 46 run with exit 0, 0 NOT MEASURED (`dispatch-gates --ran`)</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 46 commands for the 4 changed files. All ran after `pnpm install --frozen-lockfile` and a full `pnpm build` of the merged tree (exit 0), each under a 900 s timeout with `NODE_OPTIONS=--max-old-space-size=8192` and its exit code captured before any pipe. - All 46 exited 0, among them `check-skills-token-ratchet.mjs`, `check:skill-docs`, `check:skill-refs`, `check:skill-identifier-liveness`, `check:doc-authoring`, `check:dts-closure`, `check:dual-build-cjs-loads` and `check-comment-mask-corpus.mjs`. - `--ran` printed: `46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED`. - Heads `87e83df5` (pre-merge) and `99852c06` (the reviewed head) also ran all 46 with exit 0; `a7da7616` changes only `objectstack-data/SKILL.md` and the changeset text, on the same merged tree. - `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD`: exit 0 (3 commits) before each push. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. (No names added.) - [x] 3. No formatter ran. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. (`main` merged in, no rebase.) - [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` not touched; nothing to regenerate. - [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. (The 维护者速读 section is Chinese, per the lane template.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8VVAjPD6BESVmRJphXXyw <!-- signed: domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0373715 commit cef3b8f

4 files changed

Lines changed: 28 additions & 17 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): three skills describe a bare field ref in a predicate as the fault it is today, not a silent `null`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`objectstack-platform/references/operations.md`, `objectstack-data` and `objectstack-ui` said a bare field ref (`done` instead of `record.done`) "evaluates to `null`" and silently hides or never fires. It faults. On the server an `error` validation rule and a `requiredWhen` refuse the write and name the rule, an option `visibleWhen` refuses only a write that picks that option (forms still offer it), a `readonlyWhen` field stays editable on the form while the server drops the change with a warning and the save succeeds, and a formula reads `null` with a logged warning. In the console an action's `visible` hides it, its `disabled` greys it out, and a field `visibleWhen` shows the field, each with a console warning. The data skill no longer lists RLS / sharing predicates here: their compiler reads a bare name as the column. The prevention advice (`os validate` / `os build`, `record.<field>`) is unchanged. Growth is paid by same-file deletions of restated prose.

‎skills/objectstack-data/SKILL.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export const Invoice = ObjectSchema.create({
202202
set → field permission `when`, rules/security.md).
203203
- **`readonly: true` governs the end-user surface, not trusted system writers.**
204204
A non-system write (REST/UI, and any `runAs:'user'` flow — the default) has
205-
the field **stripped** from any non-system write; the write reports
205+
the field **stripped**; the write reports
206206
success but the value never lands. System-context writes —
207207
`runAs:'system'` flows, system hooks, seeds, imports, migrations — are exempt
208208
and DO write it. So the pattern "users can't edit this, but automation
@@ -447,13 +447,15 @@ os validate # Zod schema + CEL predicates (record.<field> existence) + bindi
447447
# or: os build # the same gates, plus emits dist/
448448
```
449449

450-
It catches what otherwise fails **silently at runtime**: a bare field ref in a
451-
`requiredWhen` / `readonlyWhen` / `visibleWhen`, a validation rule, a formula, or
452-
a row-level-security/sharing predicate (`done` instead of `record.done`) that
453-
evaluates to `null` and never fires. `os lint` is a *separate*
450+
It catches what otherwise surfaces only at runtime, where a bare field ref
451+
(`done` for `record.done`) faults: an `error` validation rule or `requiredWhen`
452+
refuses the write naming it, an option `visibleWhen` only a write picking the
453+
option (forms still offer it); a `readonlyWhen` field stays editable and the
454+
save succeeds without its change (logged); a formula reads `null` (logged).
455+
`os lint` is a *separate*
454456
pass that additionally checks the data model against the conventions in this
455-
skill (relationships, master-detail, roll-ups) — run it too, but it does **not**
456-
replace `os validate`. In a scaffolded project the gate is `npm run validate`.
457+
skill (relationships, master-detail, roll-ups) — run it too.
458+
In a scaffolded project the gate is `npm run validate`.
457459

458460
---
459461

‎skills/objectstack-platform/references/operations.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424

2525
## Verify your work
2626

27-
ObjectStack metadata mistakes fail **silently at runtime**, not at edit time:
28-
a bare field ref in a predicate (`done` instead of `record.done`) evaluates to
29-
`null` and silently hides an action/validation on every record; a
30-
dangling dashboard widget binding renders an empty chart (ADR-0021). Both are
31-
caught at author time by one command:
27+
Metadata mistakes surface at runtime, not at edit time: a bare field ref in a
28+
predicate (`done` for `record.done`) faults, so an `error` validation rule
29+
refuses each write naming the rule and an action's `visible` hides it; a
30+
dangling dashboard widget binding renders an empty chart. Both are caught at
31+
author time by one command:
3232

3333
```bash
3434
os validate # Zod schema + CEL predicates + widget bindings — no artifact

‎skills/objectstack-ui/SKILL.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,18 +264,17 @@ os validate # CEL predicates (record.<field>) + widget bindings + schema
264264
# or: os build # the same gates, plus emits dist/
265265
```
266266

267-
Two UI-specific traps it catches, both **silent at runtime** otherwise:
267+
Two UI-specific traps it catches, otherwise met only at runtime:
268268

269269
- **Action / field predicate** — a bare field ref in an action `visible` /
270270
`disabled` or a field `visibleWhen` (`done` instead of `record.done`)
271-
evaluates to `null` and hides the control on *every* record (the
272-
"button never shows" trap).
271+
faults on *every* record: the action hides (`visible`) or greys out
272+
(`disabled`), the field shows; the console logs the fault.
273273
- **Dashboard widget binding** — a widget `dataset` / `dimensions` / `values`
274274
that doesn't resolve to a declared dataset/field renders an empty chart
275275
(ADR-0021).
276276

277-
Don't report a view/action/dashboard done until `os validate` passes. In a
278-
scaffolded project the gate is `npm run validate`.
277+
In a scaffolded project the gate is `npm run validate`.
279278

280279
---
281280

0 commit comments

Comments
 (0)