fix(objectql,lint): 服务端为 requiredWhen 绑定 parent 作用域,并把构建期硬闸扩到同一格 (#4977) - #6440
Conversation
…e it at build time (#4977) #4889 closed the `parent`-scope hole for `readonlyWhen`. `requiredWhen` sits on the same field, is evaluated by the same module, and still had it: nothing bound `parent`, so `requiredWhen: parent.status == 'sent'` faulted, took the fail-open branch, and the write landed with the field empty. Per the maintainer's 2026-08-06 ruling, A + C and deliberately NOT symmetric with #4889: - A: the engine resolves the master-detail header with #4889's own `resolveMasterDetailParent(s)` and passes it to the evaluator on insert, single-id update and bulk update. Evaluation semantics are unchanged — an unevaluable predicate (unresolvable header included) stays fail-OPEN. Option B (422) was explicitly not taken; it is reserved for ADR-0058 D5. - C: `@objectstack/lint`'s parent-scope gate, previously scoped to `readonlyWhen`, now judges `requiredWhen` too. One gate, two consequence clauses: the runtimes fail in opposite directions, so a shared message would prescribe the wrong fix. `previousParent` is added for the ADR-0113 non-regression pre-check, which asks about the STORED row and therefore needs the header that row hung off — binding the landing header there would read a repoint onto a Sent header as a pre-existing violation and let it rest. The object-level `script` / `cross_field` rules sharing this evaluation site do NOT get the root: they are fail-CLOSED since #4649, and binding one there would flip writes they reject today into accepted ones. Pinned by test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…uired-when-parent-scope
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 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:
|
…uired-when-parent-scope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…kageId
新增的 4 处 `engine.registry.registerObject({...} as any)` 只传了 1 个实参,
而签名是 `registerObject(schema, packageId, namespace?, ownership?, priority?)`
—— `packageId` 是必填。tsc 在 test 层因此报 4 条 TS2554
(`Expected 2-5 arguments, but got 1`,行 115/125/275/318),把
`@objectstack/objectql` 的 TEST_DEBT 实测值顶到棘轮记录值之上。
按包内既有写法补 `'test-package'`(与 query-expression-conformance.test.ts、
save-meta-response-conformance.test.ts 一致),不引入宽容 helper、不改运行时语义。
实测:objectql test 层 raw tsc 由 355 降到 351,与 origin/main 基线逐行相同
(comm 差集为空),即本 PR 对该账目的净贡献为 0,较记录值 355 留出 4 的余量。
该文件 14 条测试全绿,包内 142 文件 / 2366 条全绿。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
|
ACCEPT(engine-core 席 #6019,会话 ① 偏差裁定(正文「偏差说明」项)——成立,核准。 派单红线「⛔ 不触 ② 分支 CI 唯一红灯(TEST_DEBT 棘轮)已由接管修复清除。 原会话亡故后由接管 dev 收尾(
③ 验收结论:裁决 A+C 落地形态、fail-open 语义不动、 Generated by Claude Code |
Fixes #4977
按维护者 2026-08-06 裁决执行 A + C;B(不可求值 422 拒写)明确不做,留 ADR-0058 D5 复审。
前提复核(三条,全部成立)
rule-validator.ts的 requiredWhen 分支求值上下文只有{ record: merged, previous }。反向验证实测:去掉本 PR 的 parent 传参,新增的 4 条命中用例立刻转红。resolveMasterDetailRelation/resolveMasterDetailParent(s)/readsParentRoot/unknownVariableOf全部原样复用,本 PR 没有新建第二套。requiredWhen只出现在 lint 的一条测试 fixture 里;showcase 的 invoice line 用的是行作用域record.quantity >= 100(invoice.object.tsL212,原注释已写明 "ROW-scoped")。属补潜伏缺口,不改变任何现有 app 行为。肢 A —— 服务端绑 parent(求值语义不动)
引擎在三个调用点解析主表头并传入求值器:insert(批量一次读)、单 id update(与 readonlyWhen 共用同一次解析)、bulk update(每行一个表头,批量一次读)。
不可求值仍然 fail-open —— 记日志、跳过、放行。唯一变化是诊断:未绑定的根会被点名(复用
unknownVariableOf),因为「表头读不到」和「作者写错 key」是两种故障、两种修法,而它们只有一行日志可分辨。previousParent:为什么多了这一个绑定ADR-0113 非回归判定问的是「写入前那一行是否已经违规」,而它挂的是旧表头。改挂(repoint)到另一个主表时,若把落地表头也喂给这个前置判定,就会把「移到 Sent 表头之下」读成既有违规而放行 —— 正是本 issue 要堵的那个收下动作,只是换了个入口。因此新增
previousParent,仅在载荷确实改挂时由引擎解析,其余情况沿用同一行、不多付一次读。爆炸半径(#4972 当初把本改动挡在范围外的原因)
对象级
script/cross_field规则共用这个求值调用点,自 #4649 起对不可求值谓词 fail-closed。本 PR 没有给它们绑新根 —— 绑了会把它们今天拒掉的写入翻成接受。已由 pin 测试钉住(单元 + 端到端各一条),实测报错仍是 #4649 的原文案。肢 C —— 构建期硬闸
packages/lint的 parent 作用域闸原本刻意只盖readonlyWhen,现在同样判requiredWhen。两格共用同一道闸(同一个masterDetailCount+readsParentRoot,CEL 解析走 formula 规范入口collectCelRootIdentifiers,未直连 cel-js),但文案分岔:两边运行时失败方向相反(readonlyWhenfail-closed ⇒ 字段永远写不进;requiredWhenfail-open ⇒ 要求永远不生效),文案指错了就等于给了相反的修法。这正是运行时敢保持 fail-open 的前提 —— 会无声烂掉的那条声明进不来。实现上两个槽位都保持字面量成员读取(
f.readonlyWhen/f.requiredWhen),没有改成f[key]索引:后者会静默解除 #5017 那道扫源码的 meta-test 的武装。测试
新增/改写覆盖(命中 / 不命中 / 缺 parent fail-open / repoint / bulk / 对照 / 爆炸半径):
packages/objectql/src/engine-required-when-parent.test.ts(新,14 条,真引擎 + 真 driver 端到端,driver fixture 沿用 Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 的)packages/objectql/src/validation/rule-validator.test.ts(+16 条单元:11 条求值语义 + 5 条 gate)packages/lint/src/validate-expressions.test.ts(+5 条)fixture 处置:原
it('is scoped to readonlyWhen — requiredWhen/visibleWhen verdicts are unchanged')钉的恰好是本 PR 删掉的那条肢,断言会因为「什么都没产出」而继续通过。按整体替换处理 ——requiredWhen半边移入新增的正向用例,此处保留的是真正没变的visibleWhen,fixture 也只声明这一个槽位。反向验证(方向先写死,两肢分别做)
诚实标注:C 肢另有两条「断言 0 个 issue」的负向 pin,在该回退下按构造必然保持绿 —— 它们不构成证据,分量全在那三条正向用例上。
命令与输出
以上为 merge
origin/main之后重跑的结果;merge-tree 预检 exit 0(无冲突)。与同批在飞单的交互(实测)
assertNoStrictDrops()→evaluateValidationRules;本 PR 只在第一步之前多解析一个表头行,不碰suppliedValues。text型字段不做类型校验,{ title: { $in: [...] } }原样写进库(number型会响亮拒绝) #5922(算子对象拒收):无交互。validateRecord在单 id update 路径上位于evaluateValidationRules之前(engine.ts L5866 vs L6000),算子对象在到达本 PR 的代码前就已被拒。operator-object-write-value.test.ts与engine-update-dispatch.test.ts一并重跑,214/214 绿。data.id是算子对象 +multi: true时,{"$in":[...]}作为普通列进入updateMany的 SET 载荷,写向主键列 #6262(data.id算子对象 + multi):落点在派发层与 bulk 分支的 SET 载荷;本 PR 在 bulk 分支只上提了parentForRow的解析并加了一个 opts 字段,与 SET 载荷的构造互不相干。merge-tree 对当前origin/main无冲突。偏差说明(需 PM 确认)
派单要求 ⛔ 不触
engine.ts。此项与裁决 A 不可兼得:evaluateValidationRules是纯函数、不持有 driver,只有引擎能读表头,而engine.ts是全仓唯一的接线点(已 grep 确认无第二处调用方)。若严守该约束,交付物就只剩「求值器能接受 parent 但没人传」——declared ≠ enforced原样留在原地。故按裁决执行,并把
engine.ts的改动压到最小、且刻意避开 #6262 的落点。Generated by Claude Code