Skip to content

docs(skills): a bare field ref in a predicate faults, it does not read null - #31028

Draft
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-rzfmz0
Draft

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-rzfmz0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Irene · project thread

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 Parent-scoped readonlyWhen is unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #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.

Local gates on a7da761 (this head): 46 derived, 46 run with exit 0, 0 NOT MEASURED (`dispatch-gates --ran`)
  • 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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema touched.)
  • 2. Every name I added uses the casing for its kind. (No names added.)
  • 3. No formatter ran.
  • 4. One issue, one branch off an up-to-date main, one pull request. (main merged in, no rebase.)
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec not touched; nothing to regenerate.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is Chinese, per the lane template.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8VVAjPD6BESVmRJphXXyw

…d null

Three skills said a bare field ref (`done` instead of `record.done`)
evaluates to null and silently hides an action or never fires a rule.
On main it faults: an error validation rule, requiredWhen and an option
visibleWhen refuse the write naming the rule, readonlyWhen holds its
field locked, a formula reads null with a warning, and in the console an
action's visible hides it, disabled greys it out and a field visibleWhen
shows the field, each with a console warning. RLS / sharing predicates
read a bare name as the column, so the data skill no longer lists them.

Growth is paid by deleting restated prose in the same files.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAjPD6BESVmRJphXXyw
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 99852c0 · 2026-11-06T18:38Z

The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent. No review seat (lanes/review.md) is seated.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 99852c067bf6085fb32b4922b79f397f904f901a
Local-runs: none
Scope: skills/objectstack-data/SKILL.md, skills/objectstack-platform/references/operations.md, skills/objectstack-ui/SKILL.md + .changeset/30893-bare-field-ref-symptom.md, packages/objectql/src/validation/rule-validator.ts, packages/objectql/src/cel-fault.ts, packages/objectql/src/engine-top-level.ts, packages/objectql/src/engine-readable-ids-permission-formula.ts, packages/formula/src/stdlib.ts, packages/formula/src/cel-engine.ts, packages/formula/src/cel-to-filter.ts, packages/plugins/plugin-security/src/rls-compiler.ts, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts, packages/lint/src/validate-expressions.ts, ui/packages/react/src/hooks/useExpression.ts, ui/packages/core/src/evaluator/ExpressionEvaluator.ts, ui/packages/core/src/evaluator/fieldRules.ts, ui/packages/core/src/evaluator/listConditional.ts, ui/packages/core/src/evaluator/predicateInput.ts, ui/packages/core/src/actions/ActionEngine.ts, ui/packages/components/src/renderers/action/action-button.tsx, ui/packages/components/src/renderers/action/action-icon.tsx, ui/packages/components/src/renderers/action/action-group.tsx, ui/packages/components/src/renderers/action/action-menu.tsx, ui/packages/components/src/renderers/action/action-bar.tsx, ui/packages/components/src/renderers/complex/data-table.tsx, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/plugin-grid/src/components/RowActionMenu.tsx, ui/packages/plugin-detail/src/RelatedList.tsx, ui/packages/plugin-detail/src/renderers/record-quick-actions.tsx, ui/packages/app-shell/src/views/DeclaredActionsBar.tsx, ui/packages/app-shell/src/views/RelatedRecordActionsBridge.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/app-shell/src/hooks/useNavRunAction.ts, ui/packages/app-shell/src/providers/ExpressionProvider.tsx, ui/packages/react/src/utils/visibilityDiagnostic.ts, ui/packages/react/src/SchemaRenderer.tsx, scripts/check-skills-token-ratchet.mjs, skills/objectstack-{data,ui,platform}/evals/*.json

Read on origin/main (7d9f1e973; the two commits past the merge-base 33bc8ae5f touch none of the three files nor the ratchet script). Net diff at the head: 4 files, the three skills plus one new changeset. No code, schema or eval file changes.

① Derived judgments

Teaching-text only. No schema, no package export, no runtime accept-set moves. The public surface that moves is the published @objectstack/skills text an agent reads. Each changed statement is judged against main below.

  • Mechanism: a bare done faults, it does not read null. Right. buildScope (packages/formula/src/stdlib.ts) mounts record / previous / input and the namespaces only, with no field flattening. A bare identifier is Unknown variable: done. The "evaluates to null" wording traces to the Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928-era docblock on firstUndeclaredReference (cel-engine.ts), which describes the historical class.
  • error validation rule: write refused, naming the rule. Right. rule-validator.ts docblock "Fail-CLOSED for unevaluable predicates (Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649)". evaluateValidationRules pushes the reason: 'unevaluable' violation into errors only at severity === 'error' and throws ValidationError. A warning / info rule is logged through unevaluableAdvisoryMessage ("write proceeded: the rule is advisory") and is not even recorded as an advisory event. The new text claims only the error case, which is correct.
  • requiredWhen: refused. Right. In evaluateValidationRules, every fault takes the D2 arm (unevaluableFieldRuleError('requiredWhen', …)), "Refused whatever the write does to the field".
  • Option visibleWhen: refused. Right, but scoped narrower than the wording (see ④ should-fix 1). evaluateOptionVisibility iterates pickedGatedOptions(fields, data), so only a write that picks a gated option is judged. A bare ref does not read the user root, so the no-acting-user admission does not apply, and the write is refused via unevaluableOptionGateError.
  • readonlyWhen: locks, does not refuse. Right on the server. In isReadonlyWhenLocked, unknownVariableOf(res.error) is truthy for ANY Unknown variable: X, not just parent, so the function returns true (LOCKED) with a warn. stripReadonlyWhenFields then drops the incoming change ("ignoring incoming change") on UPDATE. The client is the other way round: READONLY_WHEN_FAULTED = false in fieldRules.ts, so the form renders the field editable. @objectstack/lint's own message (validate-expressions.ts, the readonlyWhen entry) states the observable: "the field looks writable, the save reports success, and the value silently never lands" (see ④ should-fix 2).
  • Formula field: reads null, logged. Right. applyFormulaPlan (engine-top-level.ts) does rec[fp.name] = null; onFault?.(…). reportFormulaFault logs logger.warn once per object and field.
  • Console action visible: hidden, warned. Right, on every action surface I found. useCondition(…, { throwOnError: true }) on action-button, action-icon, action-group (members and host), action-menu (members and host), action-bar, DeclaredActionsBar.useDeclaredActionShown, useNavRunAction, and the RelatedList toolbar. ActionEngine.getActionsForLocation and record-quick-actions use throwOnError. The row/header surfaces use evalRowPredicate / useRowPredicate with fallback: false: data-table, plugin-grid RowActionMenu, RelatedRecordActionsBridge, and containers.tsx page:header visible. The row is bound only as record (usePredicateRecordContext), so done is unbound everywhere. SchemaRenderer's node-level shouldHide fails soft (shown), but the renderer's own fail-closed gate sits beneath it, so the net result is still hidden. I found no surface that renders an action's visible fail-open.
  • Console action disabled: greyed out, warned. Right. The non-throwing useCondition → evaluateCelCondition fast path is evalFieldPredicate(source, record, true, …), which warns in production. Row/header: data-table L662 and RowActionMenu L404 use fallback: true, and containers.tsx header disabled uses true. The built-in Edit/Delete disabledWhen (RecordDetailView, DetailView) fail soft to enabled. Those are userActions predicates, not an action's disabled, so they are outside the claim.
  • Field visibleWhen on a form: shown, reported. Right. VISIBLE_WHEN_FAULTED = true (fieldRules.ts). evaluateVisibility / isObjectFieldVisible (ExpressionProvider.tsx) are fail-open with reportUnresolvableVisibilityPredicate (console.warn, production leg included). The render side of ADR-0137 D3 matches the rule-validator docblock.
  • RLS / sharing: removed from the trap list. Right. compileCelToFilter operand case 'id' reads "Bare identifier = a single record field (RLS convention)". RLS (rls-compiler.ts compileExpressionOutcome) and declared sharing rules (declared-sharing-rules.ts) both lower through it, so a bare region is the column, the same as record.region.

② Semver level

'@objectstack/skills': patch matches what the diff publishes. packages/skills is the package that ships skills/** (precedent: .changeset/28804-data-eval-field-patches.md). The change is prose-only: no schema, no accept-set, no export. Clause-②: no is correct: nothing published is narrowed or widened, and no protocol field changes. Metadata-protocol: none is correct. Both lines appear in the PR body and in the changeset. Check Changeset is green on the head.

③ Boundary flags

  • Deviation 1 (card: action visible fails OPEN; dev: fails CLOSED). The dev is right on main. The card attributed the fail-open answer to evaluateVisibility. On main that function is the field / nav gate. Every action renderer and ActionEngine hides on a fault with throwOnError / fallback: false (objectui#11212 moved action:group, action:icon and the related-list toolbar onto it). I checked the row, header, bar, menu, group, icon, quick-actions, declared-bar and nav-run surfaces and found no fail-open leg for an action's visible.
  • Deviation 2 (readonlyWhen with a bare ref locks rather than the D2 refusal). The dev is right on main. isReadonlyWhenLocked keys the LOCK on unknownVariableOf, which matches any unbound root, and a bare done is one. The D2 refusal is reached only by the other faults (No such key, null overload, parse). The rule-validator docblock frames the carve-out as the parent case, but the code is broader. @objectstack/lint's ledger table (validate-expressions.ts ≈L1371, "requiredWhen, readonlyWhen … refuses the write") disagrees with its own readonlyWhen message (≈L1097, LOCKED). That is a lint-internal inconsistency outside this PR. It is a residue, not a finding here.
  • Deviation 3 (RLS / sharing dropped). The dev is right on main. Both compile through compileCelToFilter, where a bare name is a column. os validate still rejects a bare ref in a sharing rule (operations.md, gate 2, unchanged), which is canon enforcement and not a runtime trap. Nothing the data skill teaches is lost by dropping the RLS / sharing entry.
  • Tier flag ("developed below the contract-review tier; maintainer's standing order waives the mandatory tier"). This record is served at CONTRACT_REVIEW_TIER and is the tier-level review of the skills/** face. Tier H approval (os-zhuang / hotlong) stays owed, whole-PR.
  • Serial flag (PR docs(skills): teach os deploy, the managed-package dev loop, in the platform skill #31019 also edits operations.md and raises its ratchet row). Confirmed: docs(skills): teach os deploy, the managed-package dev loop, in the platform skill #31019 has not landed on main (no commit past the merge-base touches the file). At this head operations.md counts 3106 against its unchanged ceiling of 3108. Whichever PR lands second merges main and re-measures, as the body says.
  • Probe flag (the dev ran a throwaway vitest on main, not committed). Not re-run here. Every outcome it printed is confirmed by reading the code paths above.

④ Design judgment

skills/objectstack-platform/references/operations.md — Holds. "faults, so an error validation rule refuses each write naming the rule and an action's visible hides it" matches evaluateValidationRules and every console action surface. Prevention advice is kept: os validate / os build, gate 2's record.FIELD check, and the "never report … done until os validate passes" rule. Deletions: the lead "ObjectStack", and "(ADR-0021)" in the intro, which survives at gate 3 "Widget-binding integrity (ADR-0021)" in the same section. Token count 3108 → 3106 with ceiling 3108 unchanged; there is no release-stamp line.

skills/objectstack-ui/SKILL.md — Holds. "faults on every record: the action hides (visible) or greys out (disabled), the field shows; the console logs the fault" survives every attempt I made to break it (see ①). A disabled fault is fail-soft true = greyed out, and a field fault is VISIBLE_WHEN_FAULTED = true. All three legs warn through console.warn in production. The prevention advice is kept: the os validate / os build block with record.FIELD, and "done instead of record.done". Deletion: "Don't report a view/action/dashboard done until os validate passes." It survives in the same section's opener ("run the author-time gate before reporting done") and in operations.md's rule of thumb. It is a real removal of a restatement, and no rule is lost. Token count 3544 → 3533 with ceiling 3544 unchanged (the 20-byte stamp line is excluded per the script's convention, and the counts match the PR body).

skills/objectstack-data/SKILL.md — Holds, with two should-fixes on precision. The validation-rule, requiredWhen, formula and form-visibleWhen statements are exact. Deletion: "but it does not replace os validate". The fact survives at operations.md ("os lint is a separate style/convention pass and does not replace os validate"), and the remaining "separate pass that additionally checks … run it too" still sets os lint beside the gate, not in place of it. Accepted. The prevention advice is kept: the os validate / os build block with record.FIELD existence, and "done for record.done". Token count 6068 → 6066 with ceiling 6078 unchanged (stamp excluded).

  • should-fix 1 (data SKILL.md L451–452): "option visibleWhen refuses the write naming it" applies only to a write that PICKS that option (pickedGatedOptions). Meanwhile the console keeps offering the option (D3, fail-open render). "a write picking the option" fits in the 12-token headroom.
  • should-fix 2 (data SKILL.md L452–453): "a readonlyWhen locks its field" is the server's verdict. The observable is the opposite in the form: READONLY_WHEN_FAULTED = false leaves the field editable, the update reports success, and the change is dropped (server-side warn only). Lint's own readonlyWhen message says exactly this. Stating the dropped-on-save symptom would point a debugging agent the right way, which is the card's business test.

Evals: the evals/*.json and evals/README.md of the three skills hold no case grading the "evaluates to null" / "never shows" / "silently hides" claim. The predicate-related cases (data #4, ui #3) grade only the record. spelling. No must-fix.

Budget: all three files are within their unchanged ceilings. Line breaks cost the same bytes as spaces, so no rewrap is counted as payment. Every removed clause has a named surviving home.

Residue (outside this card's three-file surface, not findings against this PR; filing candidates):

  • packages/create-objectstack/src/templates/AGENTS.md L16–18 still says "evaluates to null and silently hides an action". This template ships to every scaffolded project.
  • content/docs/deployment/validating-metadata.mdx L31 and L38 ("resolves to null", "the rule never fires").
  • The lint ledger row for readonlyWhen noted in ③.

Check-runs on the head: 40 total, 23 success, 13 skipped, 4 in progress (Build Core, Test Core (1/8), Test Core (2/8), Live E2E UI). None failed. Every lint family (the docs & code guards and repo & tooling gates that carry the skills ratchet and skill-doc checks), all Type Check legs, Check Changeset, PR-body guards, Governed Surface Queue Guard and Lint · PM dispatch-gates are green.

Implemented-by: claude/project-thread-rzfmz0
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

…onlyWhen does on the form

An option visibleWhen that faults refuses only a write that picks that
option, and forms keep offering it. A readonlyWhen that faults on a bare
ref keeps the field editable on the form, the save succeeds, and the
server drops the change with a warning. Paid by dropping the form-side
visibleWhen clause (the ui skill states it) and a self-restating
"from any non-system write" in the readonly bullet.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAjPD6BESVmRJphXXyw
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head a7da761 · 2026-11-06T19:00Z

Delta review against the PASS at head 99852c0 (comment 6692714830, should-fixes S1 and S2). The delta is one commit answering both, plus the PR body. The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a7da76162bcef12b7901e16974df6d3f7be025e6
Local-runs: none
Scope: skills/objectstack-data/SKILL.md, skills/objectstack-platform/references/operations.md, skills/objectstack-ui/SKILL.md + .changeset/30893-bare-field-ref-symptom.md, packages/objectql/src/validation/rule-validator.ts, packages/objectql/src/engine.ts, packages/objectql/src/cel-fault.ts, packages/lint/src/validate-expressions.ts, ui/packages/core/src/evaluator/fieldRules.ts, ui/packages/core/src/evaluator/optionRules.ts, scripts/check-skills-token-ratchet.mjs

This is a delta review. The head is one commit (a7da76162) on 99852c067, which carried the prior PASS record. The delta touches skills/objectstack-data/SKILL.md (two hunks) and the changeset text. Main was not merged in. The net diff against the merge-base 33bc8ae5f is still the same 4 files. origin/main (now 41dc30249) has no commit past the merge-base that touches the three skill files or the ratchet script, and PR #31019 has not landed. Everything the prior record judged on operations.md and objectstack-ui/SKILL.md is unchanged and stands.

① Derived judgments

The change is teaching text only. No schema, export or runtime accept-set moves. The two re-worded symptoms are judged against main:

  • S1: option visibleWhen. Now true. The new text says an option visibleWhen refuses "only a write picking the option (forms still offer it)".
    • Server: evaluateOptionVisibility iterates pickedGatedOptions(fields, data), so only a written choice whose option carries visibleWhen is judged. A bare done reads no user root, so the no-acting-user admission does not apply, and unevaluableOptionGateError refuses the write.
    • Client: the rule-validator D2/D3 docblock says "a faulting option predicate leaves the option offered". optionRules.ts agrees: evalFieldPredicate(o.visibleWhen, record, true, …) keeps the option, and its docblock says "a predicate that errors fails open (kept)".
  • S2: readonlyWhen. Now true. The new text says the field "stays editable and the save succeeds without its change (logged)".
    • Form: READONLY_WHEN_FAULTED = false (fieldRules.ts), so the field is editable.
    • Server: isReadonlyWhenLocked returns true on any unknownVariableOf fault and warns "treating the field as LOCKED". stripReadonlyWhenFields then drops the key with "ignoring incoming change", and the rest of the write proceeds.
    • The strip runs on the update paths only (engine.ts single-id and multi-row update), and "without its change" fits that.
    • Lint's readonlyWhen message says the same: "the field looks writable, the save reports success, and the value silently never lands".
  • Unchanged from the prior record: an error validation rule and requiredWhen refuse the write, naming the rule. A formula reads null and logs a warning.

② Semver level

'@objectstack/skills': patch still matches: the publish is prose in skills/**, shipped by packages/skills. Clause-②: no and Metadata-protocol: none are both correct and unchanged in the PR body and the changeset. The changeset prose matches the tree:

  • "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"

Its sentence "In the console … a field visibleWhen shows the field" describes the three skills together, and objectstack-ui/SKILL.md Verify still states it.

③ Boundary flags

  • Dev flag: S1 and S2 fixed. Confirmed against the code (①).
  • Dev flag: 6078/6078, headroom 0, no row raise. Confirmed in ④.
  • Dev flag: main not merged. Confirmed. The merge-base is still 33bc8ae5f, and main has not touched these paths since.
  • Dev flag: two same-file deletions pay the growth. Judged in ④. Neither loses a rule.
  • PR body vs tree. The "What changes" quote, the claim → code-site rows for S1 and S2, the token table (data 6068 → 6078, ceiling 6078) and the deletions list all match the head. One deletion entry overstates its homes (should-fix 1).
  • Prior deviations 1–3 (action visible fails closed, readonlyWhen locks, RLS/sharing dropped). Untouched by this delta, and they stand as judged.

④ Design judgment

skills/objectstack-data/SKILL.md — Holds.

Deletion A: "from any non-system write" in the readonly: true bullet. No rule lost.

  • The sentence still reads "A non-system write (REST/UI, and any runAs:'user' flow — the default) has the field stripped; the write reports success but the value never lands".
  • The very next sentence keeps the exemption verbatim: "System-context writes — runAs:'system' flows, system hooks, seeds, imports, migrations — are exempt and DO write it".
  • So the deleted words were a pure restatement of the sentence's own subject. The lines around the cut were not reflowed; the shortened line was just left short.

Deletion B: "a form shows a visibleWhen field" in Verify. No rule lost.

  • The fact survives verbatim at the named home in objectstack-ui/SKILL.md Verify: "a field visibleWhen … faults on every record: … the field shows; the console logs the fault".
  • The prevention rule that covers this site stays in this file: os validate / os build with record.FIELD existence, and "done for record.done".
  • The deleted text was never on main. This PR added it at 87e83df5 as a duplicate of the ui skill's statement. Main's only text for the site was the stale "evaluates to null" claim.
  • A cross-skill home was accepted for the earlier os lint deletion on the same grounds.

should-fix 1:

  • The PR body's second home for Deletion B is this file's L199, "Use visibleWhen to hide irrelevant fields in ObjectUI forms". That line states no fault symptom, so it is not a home for the fact.
  • After the cut, the data skill's Verify names every field-level site except the field visibleWhen. Yet the frontmatter (L10) routes visibleWhen rules to this skill, and the card listed visibleWhen among the data passage's sites. An agent authoring *.object.ts is pointed at the right gate but not told the symptom.
  • Fix in the body: drop the L199 claim. Optionally, in a later ratchet-funded edit, restore a four-word pointer such as "a form still shows a faulted field visibleWhen".

should-fix 2:

  • "an option visibleWhen only a write picking the option" drops "refuses" (and "naming it"). The sentence is grammatical, but a reader can misparse it.
  • Repeating "refuses" costs about 2 tokens, which needs another same-file cut at headroom 0.

Budget. The ceiling and both counts were re-measured by hand, using ceil(bytes/4) minus the 20-byte release-stamp line, as the script does for a stamped SKILL.md:

  • scripts/check-skills-token-ratchet.mjs L527 still reads ['skills/objectstack-data/SKILL.md', 6078], unchanged.
  • At this head the file is 24332 bytes, which counts to 6078. That equals the ceiling.
  • At 99852c06 the count was 6066; on main it was 6068.
  • operations.md (3106 / 3108) and objectstack-ui/SKILL.md (3533 / 3544) are untouched.
  • No rewrap was counted as payment: under bytes/4 a newline costs the same as a space.

Evals. The delta touches no eval. The prior finding stands: no case in the three skills grades the old "evaluates to null" claim.

Check-runs on a7da76162: 44 total. 28 passed, 15 were skipped, and 1 is still running (Test Core (1/8)). None failed. All lint families, every Type Check leg, Check Changeset, PR-body guards, Governed Surface Queue Guard and Lint · PM dispatch-gates are green.

The residue the prior record named, all outside this card, still stands:

  • packages/create-objectstack/src/templates/AGENTS.md L16–18
  • content/docs/deployment/validating-metadata.mdx L31/L38
  • the lint ledger row for readonlyWhen

Implemented-by: claude/project-thread-rzfmz0
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

维护者速读

  • 改了什么: 三个技能(objectstack-data、objectstack-ui、objectstack-platform 的 operations.md)原说「谓词里写裸字段(done 而非 record.done)会求值为 null、悄悄不生效」,改为 main 上逐处的真实表现:error 校验规则与 requiredWhen 拒绝写入并点名规则;选项 visibleWhen 只拒绝选了该选项的写入;readonlyWhen 表单仍可编辑、保存成功但改动被丢弃(有日志);公式读 null;动作 visible 隐藏、disabled 置灰,字段 visibleWhen 显示,均有控制台告警。预防建议(os validate / os build、record.<field>)保留。4 个文件,+28/−17。
  • 为什么改: 旧说法是 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 / objectui#6443 之前的行为,现在多数情况是拒写或显示,照旧说法排查会找错方向(skills: three skills say a bare field ref in a predicate "evaluates to null" and silently hides or never fires; main fails loud #30893)。
  • 风险与回滚: 只改技能文字,无代码改动;三个文件都在原上限内(data 6078/6078、ui 3533/3544、operations 3106/3108),靠同文件删重复说法付账,没提上限。可整 PR revert。
  • 先看: skills/objectstack-data/SKILL.md 的 Verify 段。
  • 两分钟验证: grep -rn "evaluates to \null`" skills/objectstack-data/SKILL.md skills/objectstack-ui/SKILL.md skills/objectstack-platform/references/operations.md` 无结果。
  • 需要你做的: skills/** 是受治理 Tier H 面,需 os-zhuang 或 hotlong 在 GitHub 上 Approve 一次。达档复核:全量 6692714830 PASS(S1/S2),增量 6693066423 PASS,Head-sha a7da76162bcef12b7901e16974df6d3f7be025e6。批准后本席落地。
  • 席位意见: ACCEPT。与卡片描述有三处不同(动作 visible 是隐藏而非显示、readonlyWhen 是锁字段、RLS/共享规则把裸标识符当列名),复核按 main 代码逐一确认,以代码为准。

席位核对 — ACCEPT · domain:skills#3 · head a7da76162bcef12b7901e16974df6d3f7be025e6

  1. 形态: draft → main,Fixes #30893;'@objectstack/skills': patch,Clause-②: no,Metadata-protocol: none;作者 objectstack-fleet[bot];4 个文件,在认领面之内。
  2. 前提实测: 复核逐处读 main 代码(rule-validator.ts、pickedGatedOptions、isReadonlyWhenLocked / stripReadonlyWhenFields、READONLY_WHEN_FAULTED、各动作渲染面的 throwOnError、evaluateVisibility、compileCelToFilter),新说法全部成立;三个技能的 eval 无旧说法。
  3. 门禁(钉 head a7da7616): dev 派生门禁 46 条全部 exit 0;check-runs 45 条全部完成,30 success、15 skip、0 failure。check-governed-merges:Tier H(skills/**)。
  4. 接受的偏差: 增量复核两条建议未修:正文把 data 技能 L199 列为字段 visibleWhen 出错表现的落脚点不准(实际落脚点是 ui 技能 Verify);「an option visibleWhen only a write picking the option」省了动词 refuses,语义无误。data 文件已无余量。
  5. 范围外去向: 同样的旧说法还在 packages/create-objectstack/src/templates/AGENTS.md L16–18 与 content/docs/deployment/validating-metadata.mdx L31/L38,@objectstack/lint 汇总表对 readonlyWhen 的描述与其自身提示矛盾,另行立卡。Skills-docs: self。
  6. 落地条款: Tier H。保持 draft,挂 pm:awaiting-merge,请 os-zhuang、hotlong 审批;获批后本席过落地前检、清标、ready、auto-merge;维护者直合亦可。与 docs(skills): teach os deploy, the managed-package dev loop, in the platform skill #31019 同改 operations.md,后落地者合 main 重测。

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026

This branch has not been deployed

No deployments
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 pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills: three skills say a bare field ref in a predicate "evaluates to null" and silently hides or never fires; main fails loud

1 participant