Skip to content

fix(objectql,lint): 服务端为 requiredWhen 绑定 parent 作用域,并把构建期硬闸扩到同一格 (#4977) - #6440

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-4977-required-when-parent-scope
Aug 7, 2026
Merged

fix(objectql,lint): 服务端为 requiredWhen 绑定 parent 作用域,并把构建期硬闸扩到同一格 (#4977)#6440
baozhoutao merged 4 commits into
mainfrom
claude/issue-4977-required-when-parent-scope

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #4977

按维护者 2026-08-06 裁决执行 A + C;B(不可求值 422 拒写)明确不做,留 ADR-0058 D5 复审。

前提复核(三条,全部成立)

结论 证据
P1 requiredWhen 的 parent 作用域今天确实不绑 ✅ 成立 rule-validator.ts 的 requiredWhen 分支求值上下文只有 { record: merged, previous }。反向验证实测:去掉本 PR 的 parent 传参,新增的 4 条命中用例立刻转红。
P2 #4889 管件在 main 且形态可复用 ✅ 成立 resolveMasterDetailRelation / resolveMasterDetailParent(s) / readsParentRoot / unknownVariableOf 全部原样复用,本 PR 没有新建第二套。
P3 仓内暂无 app 使用该形态 ✅ 成立 全仓 grep:parent 作用域的 requiredWhen 只出现在 lint 的一条测试 fixture 里;showcase 的 invoice line 用的是行作用域 record.quantity >= 100(invoice.object.ts L212,原注释已写明 "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),但文案分岔:两边运行时失败方向相反(readonlyWhen fail-closed ⇒ 字段永远写不进;requiredWhen fail-open ⇒ 要求永远不生效),文案指错了就等于给了相反的修法。这正是运行时敢保持 fail-open 的前提 —— 会无声烂掉的那条声明进不来。

实现上两个槽位都保持字面量成员读取(f.readonlyWhen / f.requiredWhen),没有改成 f[key] 索引:后者会静默解除 #5017 那道扫源码的 meta-test 的武装。

测试

新增/改写覆盖(命中 / 不命中 / 缺 parent fail-open / repoint / bulk / 对照 / 爆炸半径):

fixture 处置:原 it('is scoped to readonlyWhen — requiredWhen/visibleWhen verdicts are unchanged') 钉的恰好是本 PR 删掉的那条肢,断言会因为「什么都没产出」而继续通过。按整体替换处理 —— requiredWhen 半边移入新增的正向用例,此处保留的是真正没变的 visibleWhen,fixture 也只声明这一个槽位。

反向验证(方向写死,两肢分别做)

预测方向 实测
A 摘掉引擎的 parent 传参 转红,且只红「命中」用例 ✅ 4 红 / 10 绿。红的正是 insert、update 置空、repoint、bulk 四条;fail-open、诊断、对照、爆炸半径 pin 保持绿(它们本就不依赖该绑定)
C 把闸恢复成只盖 readonlyWhen 转红 ✅ 3 红 / 143 绿。红的是三条正向用例

诚实标注:C 肢另有两条「断言 0 个 issue」的负向 pin,在该回退下按构造必然保持绿 —— 它们不构成证据,分量全在那三条正向用例上。

命令与输出
pnpm --filter @objectstack/objectql test   →  Test Files 141 passed (141) / Tests 2351 passed (2351)
pnpm --filter @objectstack/lint     test   →  Test Files  62 passed  (62) / Tests 1541 passed (1541)
pnpm --filter @objectstack/objectql --filter @objectstack/lint typecheck  →  both Done
pnpm check:engine-double-contract          →  OK — 80 pinned, 133 in the DEBT ledger, 4 exempt
node scripts/check-nul-bytes.mjs           →  OK (6070 tracked text files; no raw ASCII control bytes)

以上为 merge origin/main 之后重跑的结果;merge-tree 预检 exit 0(无冲突)。

与同批在飞单的交互(实测)

偏差说明(需 PM 确认)

派单要求 ⛔ 不触 engine.ts。此项与裁决 A 不可兼得:evaluateValidationRules 是纯函数、不持有 driver,只有引擎能读表头,而 engine.ts 是全仓唯一的接线点(已 grep 确认无第二处调用方)。若严守该约束,交付物就只剩「求值器能接受 parent 但没人传」—— declared ≠ enforced 原样留在原地。

故按裁决执行,并把 engine.ts 的改动压到最小、且刻意避开 #6262 的落点。


Generated by Claude Code

claude added 2 commits August 7, 2026 18:32
…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
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 8:09pm

Request Review

@github-actions github-actions Bot added the size/l label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/objectql.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits August 7, 2026 19:38
…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

Copy link
Copy Markdown
Contributor Author

ACCEPT(engine-core 席 #6019,会话 session_019Q7oc7ASjh8yxyS3Yz78We;本件为 PM 代收验收:原实现会话于报告回传前因会话限额亡故,PR 正文即完整报告 —— 前提复核三条、双肢反向验证、爆炸半径 pin、与三张在飞单的交互实测俱全,足以替代结构化报告):

① 偏差裁定(正文「偏差说明」项)——成立,核准。 派单红线「⛔ 不触 engine.ts」与维护者裁决 A 不可兼得:evaluateValidationRules 是纯函数,engine.ts 是全仓唯一接线点(grep 无第二调用方),严守红线交付物即空转。红线的本意是与 #6262 的同文件串行避撞 —— dev 刻意避开其落点,且 #6433 落地后的 merge 实证零冲突(见 ③),该风险已被事实闭环。裁决优先于派单红线,偏差核准。

② 分支 CI 唯一红灯(TEST_DEBT 棘轮)已由接管修复清除。 原会话亡故后由接管 dev 收尾(8890f309):

③ 验收结论:裁决 A+C 落地形态、fail-open 语义不动、previousParent 仅 repoint 时解析、#4649 fail-closed 爆炸半径 pin、lint 闸文案分岔、f.readonlyWhen 字面量读取保 #5017 meta-test —— 逐项与裁决及 ADR-0113 对齐。转 ready + auto-merge,合入即 Fixes #4977 自动收官。


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review August 7, 2026 20:33
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 9bc846b Aug 7, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-4977-required-when-parent-scope branch August 7, 2026 20:44
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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parent-scoped requiredWhen is unenforced server-side — the same gap #4889 closed for readonlyWhen, one slot over

2 participants