Repository navigation
Commit cef3b8f
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
- .changeset
- skills
- objectstack-data
- objectstack-platform/references
- objectstack-ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
205 | | - | |
| 205 | + | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
| |||
447 | 447 | | |
448 | 448 | | |
449 | 449 | | |
450 | | - | |
451 | | - | |
452 | | - | |
453 | | - | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
454 | 456 | | |
455 | | - | |
456 | | - | |
| 457 | + | |
| 458 | + | |
457 | 459 | | |
458 | 460 | | |
459 | 461 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
267 | | - | |
| 267 | + | |
268 | 268 | | |
269 | 269 | | |
270 | 270 | | |
271 | | - | |
272 | | - | |
| 271 | + | |
| 272 | + | |
273 | 273 | | |
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
277 | | - | |
278 | | - | |
| 277 | + | |
279 | 278 | | |
280 | 279 | | |
281 | 280 | | |
| |||
0 commit comments