docs(lint,objectql): correct the null-guard seam ledger after #6454 — readonlyWhen is TOTAL, excluded by ruling, not by binding - #6796
Merged
os-project-manager merged 1 commit intoAug 8, 2026
Conversation
… readonlyWhen is TOTAL, excluded by ruling, not by binding
The surface ledger in validate-null-guards.ts still said field readonlyWhen
binds a sparse record ("stripReadonlyWhenFields merges {...previous, ...data}
and never materializes"). That stopped being true when PR #6454 landed the
engine-core share of the #4953 ruling: readonlyWhenBindings now runs both
roots through materializeDeclaredFields. The row now records the real state —
binding TOTAL, verdict still excluded, but by clause 3 of the ruling (both
server-side seams must be total before the gate widens; the flow
trigger-record half is not wired yet), not by the totality criterion.
The rewritten row also records the one verdict #6454 moved the OTHER way:
on a total record !has(record.b) over a declared field is uniformly FALSE,
so a lock spelled that way STOPPED locking — plus the fail-open policy
(isReadonlyWhenLocked allows the change through on a fault, except the #4889
unbound-root branch), which the widening must carry into the evidence column.
Neighbouring rows re-verified against origin/main in the same pass:
- flow / edge condition: still sparse (record-change-trigger.ts:309), but the
reason is now "not yet wired" (#4953 clause 1, services lane), not an open
question; also renames the seed's inputDoc spelling to inputData (#5671).
- action visible/disabled: still sparse, now by DECISION (#4953 clause 2).
- validation rules / hook condition / requiredWhen / sharing rules / field
expression rows: verified accurate, unchanged.
- cel-fault.ts header: "both evaluators" is three seams since #4953 (the
rider the issue named).
Comment-only change: no runtime behaviour moves, and no gate or test asserts
the ledger's text — the deliverable is that the decision input the next
author reads is true again.
Fixes #6458
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-project-manager
marked this pull request as ready for review
August 8, 2026 22:26
os-project-manager
deleted the
claude/issue-6458-null-guard-ledger-readonlywhen
branch
August 8, 2026 23:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6458
Problem
The surface ledger in
packages/lint/src/validate-null-guards.ts— the module-header table the next author reads to decide whether a seam joins the null-guard gate (#4811: "a surface excluded without a reason is indistinguishable from one nobody looked at") — still carried this row:That row became false the moment PR #6454 merged (2026-08-07 22:14 UTC). #6454's own body says it deliberately did not touch
packages/lint(quoted verbatim, per its Chinese original): 「#4811 的 null-guard 闸门扩面按裁决第 3 条要等两处服务端接缝都齐,故本 PR 不触packages/lint」— so the ledger was knowingly left behind, and this PR is the catch-up. Re-verified onorigin/main(64d764e76) before editing:readonlyWhenBindings(rule-validator.ts:474) runs both therecordandpreviousroots throughmaterializeDeclaredFields; the code the evidence column pointed at no longer exists.What the corrected row says — deliberately not "now materialized"
A one-word swap from
sparsetoTOTALwould replace one false statement with another, because #6454's measured grid was not uniform — one cell flipped the opposite way:record.b == nullhas(record.b)!has(record.b)A lock spelled
!has(record.b)stopped locking when #6454 landed. The rewritten row therefore records three facts, each traced to code:readonlyWhenBindingsmaterialises both roots (#4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953 clause 1, landed in fix(objectql): 字段readonlyWhen求值补materializeDeclaredFields—— 服务端第三个接缝 (#4953) #6454). The totality criterion is now satisfied on this face.bindingandverdictare two different questions and have come apart on this row.isReadonlyWhenLockedlogsfailed to evaluate — change allowed through; exception: the Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 unbound-root branch resolves LOCKED), so it wantsnullGuardMessage's'fail-open'outcome likerequiredWhen; and the!has(record.b)reversal above, pinned by fix(objectql): 字段readonlyWhen求值补materializeDeclaredFields—— 服务端第三个接缝 (#4953) #6454 inrule-validator.test.tsin both spellings.Neighbouring rows — checked in the same pass, verdicts stated explicitly
condition(stale, fixed): the binding is still sparse (verified:record-change-trigger.ts:309seeds{ ...(inputData ?? {}), ...after }, no materialization), but the row's standing changed with the same ruling: this is #4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953 clause 1's other half, so the exclusion is now a NOT-YET (services lane outstanding) rather than an open question. When it lands, this row and thereadonlyWhenrow flip together — exactly clause 3's condition. Also renamed the evidence'sinputDocspelling toinputData(trigger-record-change的input.doc防御性 alias 读的是一个没有生产者的键(引擎只建input.data) #5671 dropped that alias read; the old name no longer appears in the trigger), here and in the twovalidate-expressions.ts/.test.tscomments that quoted it.visible/disabled(reason upgraded): still sparse (no materialization exists in objectui on that path — unchanged), but the "requires first deciding whether the binding should be made total" sentence was stale: #4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953 clause 2 DECIDED it. The face stays sparse by ruling (totalising would mean every REST read padding out all declared columns); authors there guard withhas(). The row now cites the decision instead of asking for one.condition/ fieldrequiredWhen: checked, still true —rule-validator.ts:1370(materializeDeclaredFields(merged, fields)),hook-wrappers.ts:886/889/946, and therequiredWhenrow's fail-open note all still describe the code on main. Unchanged.condition/ fieldexpression: checked, still true — SQL compilation and the validate-expressions.ts 的 field-formula 校验读f.formula—— spec 声明的是expression,这段从未对任何 spec 合法 stack 跑过 #5026 product-judgement exclusion are untouched by fix(objectql): 字段readonlyWhen求值补materializeDeclaredFields—— 服务端第三个接缝 (#4953) #6454. Unchanged.The rider the issue named
packages/objectql/src/cel-fault.ts's header saidmaterializeDeclaredFieldssits "in front of both evaluators" — three server-side seams since #4953. Corrected in place, with the note that the third seam (fail-open) reads onlyunknownVariableOfand never this module's rejection sentences (which is why cel-fault itself needed no code change in #6454). Two lines of the same drift, same cause, folded in per the triage comment; it stayed a comment-only edit, so no lane boundary issue arose.Also updated: the
checkNullGuardsdoc block invalidate-expressions.ts, which repeated the sparse-binding claim forreadonlyWhen("its binding is sparse and!= nullwould be the wrong prescription") — now states the real blocker (clause 3) and the'fail-open'outcome the future wiring needs.Scope guard
checkNullGuardscall sites are unchanged;validate-expressions.ts/.test.tsedits are comment text only. Widening is null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811 / ruling clause 3 and waits for the services half.validate-translation-references.tsuntouched (claimed in parallel under lint:collectViewRecord的 listViews/formViews 分支收「map key + 内层 name」两种拼写,而组装器只认 map key —— 冲突改名时两者恰好相反 #6422).Verification honesty
This ledger is a decision input, not a machine-read artifact: no test or gate asserts its text, so no assertion could go red on the stale row — that is exactly why the issue was filed rather than caught by CI. The gates below prove the change breaks nothing; they cannot prove the prose true. The truth claim rests on the file:line evidence cited above, all re-checked on
origin/mainat64d764e76.Note on the Docs Drift Check advisory on this PR: it fired on package paths (any diff under
@objectstack/lint/@objectstack/objectqltriggers it), not on semantics. This diff carries no behaviour change, so none of the listed hand-written docs can have drifted from it — not actionable here, and it is advisory-only, not a required check.Comment-only, releases nothing → no changeset;
skip-changesetlabel applied (read back after the labeler settled:size/m,tests,skip-changeset).🤖 Generated with Claude Code
https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn