fix(objectql): 字段 readonlyWhen 求值补 materializeDeclaredFields —— 服务端第三个接缝 (#4953) - #6454
Merged
Merged
Conversation
`readonlyWhen` 是三个服务端 CEL 求值接缝里唯一没有物化的一个:写入路径上的
`stripReadonlyWhenFields` / `stripReadonlyWhenFieldsMulti` 把
`{ ...previous, ...data }` 原样交给求值器。于是同一字段上的 `requiredWhen`
(同文件、已物化)与 `readonlyWhen` 对「记录是什么」给出相反答案 —— 后者在驱动
未回读某已声明列时 fault,而 `readonlyWhen` fault 是 fail-open,作者声明为冻结的
字段被照常写入。
按维护者 2026-08-06 裁决(#4953 第 1 条 engine-core 份额)统一服务端接缝:
`record`(merged)与 `previous` 两个根都过 `materializeDeclaredFields`,单行与
bulk 两条路径一致。
- `parent` 表头不物化:它是另一个对象的行,且「未绑定」正是 #4889 fail-closed
判定所依赖的信号。
- 未读到前序行时不物化(与 `evaluateValidationRules` 的 groundTruth 同规则):
那样是捏造与库中行矛盾的值,而非补齐缺失值。
- 对象级 `script` / `cross_field` 的 fail-closed 与文案不变,由测试钉住。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…donly-when-materialize
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 7, 2026 21:59
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.
Part of #4953
维护者 2026-08-06 裁决第 1 条的 engine-core 份额:字段
readonlyWhen求值补materializeDeclaredFields。母单是决策锚点,不随本 PR 关闭;flow 触发记录播种(services 席)与 objectui 稀疏面文档 / lint 评估不在本 PR 内,#4811 的 null-guard
闸门扩面按裁决第 3 条要等两处服务端接缝都齐,故本 PR 不触
packages/lint。问题
materializeDeclaredFields(#1871 / #4649)只接在两个求值接缝上:evaluateValidationRules(对象级规则 + 字段requiredWhen+ optionvisibleWhen)与
hook-wrappers.ts的 hookcondition。写入路径上的stripReadonlyWhenFields/stripReadonlyWhenFieldsMulti把{ ...previous, ...data }原样交给 CEL。于是同一个字段上的两条谓词对「记录是什么」给出相反答案:
requiredWhen: record.approved_at == null是可用的守卫,同字段的readonlyWhen: record.approved_at == null只要驱动没回读该列就 fault —— 而readonlyWhenfault 是 fail-open,作者声明为冻结的字段被照常写入。是否被拦取决于驱动回读了哪些列,作者看不见也控制不了。
改动
packages/objectql/src/validation/rule-validator.ts新增readonlyWhenBindings(),把
record(merged)与previous两个根过共用的materializeDeclaredFields,单行与bulk 两条路径共用;bulk 的行视图按行构造一次(不随字段重复构造),且仅在载荷确实写了
readonlyWhen字段时才构造。packages/objectql/src/declared-fields.ts的接缝台账同步更新(哪些面已物化、哪两面仍稀疏、以及两者的区别是「尚未接线」还是「已裁决保持稀疏」)。
前提复核(动手前对 origin/main 逐条实测)
stripReadonlyWhenFields未物化,而同文件requiredWhen已物化rule-validator.ts第 418 行const merged = { ...(previous ?? {}), ...data };(bulk 同形{ ...(row ?? {}), ...data }),对照第 1186 / 1197 行materializeDeclaredFields。#6440 落地后重核形状,该函数只多了parent形参,合并仍未物化materializeDeclaredFields签名可直接复用(record, fields),fields直接传objectSchema.fields,无需第二套实现语义后果方向(实测,非断言)
改前的实测网格(稀疏前序行 = 驱动只回读写过的列):
record.b == nullprevious.b == nullrecord.b != nullhas(record.b)!has(record.b)record.a < record.bno such overload)→ 放行record.stauts(未声明键)唯一反向格
!has(record.< 已声明字段 >)的处置:实现并钉住,不静默。 理由:全量绑定下has(已声明字段)恒真是 CEL 自身规则,也是declared-fields.ts自 #4649 起写明的契约(「
has()守的是未声明的键,不是空值;判空用!= null」);它在另外两个已物化接缝上早已如此。而且它改前也不是一条保证 —— 在回读全部列的驱动上同一声明从不锁 —— 所以这次是把
一个取决于存储细节的判定换成确定的
false。两种拼写都由测试钉住,!has那条的注释写明作者应改用
== null(正是 lint null-guard 闸门一直建议的写法)。此格已在报告中单列上报。爆炸半径
parent表头不物化:它是另一个对象的行(本函数没有它的声明字段表),且「未绑定」正是 Parent-scoped
readonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 fail-closed 判定所依赖的信号。测试钉住:未绑定parent仍判 LOCKED;已绑定但表头缺键仍 fault → fail-open。
script/cross_field自 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 起 fail-closed —— 本改动完全不进evaluateValidationRules,测试钉住两者报错文案(could not be evaluated)不变。readonlyWhen剥离本就只在 update 路径(engine.ts注释「INSERT staysexempt」),故本接缝不存在 insert 格;派发词里的 insert 用例已按实际形状换成
「前序行在手但稀疏」这一真正的稀疏典型格,并另加一格钉住「前序行不在手 ⇒ 不物化」。
evaluateValidationRules的groundTruth同规则。引擎在对象声明了readonlyWhen时必取前序行(
needsPriorRecord→fieldsNeedPrior),故物化分支是常态分支。previous/priorRows是引擎的hookContext.previous,物化前先浅拷贝,测试钉住原对象不长出
null键。反向验证(方向先写死,再实测)
摘掉两处
materializeDeclaredFields调用(测试不动),预测 6 红 / 137 绿:(上面引用的用例名里
< >两侧加了空格,以免被 GitHub 正文的 HTML 标签清洗吞掉;源码中为不带空格的尖括号形式。)实测与预测逐条一致(红名单、绿名单、总数)。其中
!has(...)那条的红是反向的:摘掉物化后字段被剥离、断言的
{ amount: 999 }不成立 —— 这正是上表那一格,PR 按实际方向记录而不是套模板。恢复物化后 143/143 全绿。
命令输出
TEST_DEBT 未抬账(台账 355,实测 345);新增测试代码在 objectql 测试层 tsc 下 0 报错。
changeset
.changeset/readonly-when-total-record.md—@objectstack/objectqlpatch,正文写明这是可见行为变化及其两个方向(含
!has()写法不再锁字段与替代写法)。Generated by Claude Code