Repository navigation
#4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953
Description
Activity
交叉关联(来自 p0 #5088 的分诊,仅记录,不认领 —— 本单仍属
domain:engine车道队列):#5088 的「Expected」末句要的正是本单的东西:「If a hook must run for a missing record, the merged record should be recognisable as incomplete so #4775 does not attribute the gap to the hook's author.」 —— 即谓词看到的记录是否全量,应当是可判定的,而不是靠调用方运气。
需要说清的是两者的分工,免得被读成同一个修法:
- data: updateMany runs hooks for a nonexistent id — the row fails INTERNAL_ERROR from a hook-condition abort instead of RECORD_NOT_FOUND #5088 的主修法不碰本单的面:它的落点是
packages/metadata-protocol/src/protocol.ts三个 bulk 写面缺少 data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435 的逐行存在性探针 —— 探针补上之后,不存在的 id 根本不会进入 hook 管线,本单报告的症状随之消失。派发词已明确限定文件面为protocol.ts,不动hook-wrappers.ts/rule-validator.ts/record-change-trigger.ts。 - 本单要解决的是更一般的那半:即使写入路径都修对了,「全量 vs 稀疏」仍是五个求值接缝各自为政、且没有任何东西标明某个 slot 属于哪一类。data: updateMany runs hooks for a nonexistent id — the row fails INTERNAL_ERROR from a hook-condition abort instead of RECORD_NOT_FOUND #5088 只堵住其中一个入口,不构成本单的答案。
一条对本单有用的旁证,来自 #5088 的实测:
protocol.ts上 #4435 的「按行诚实」只落在 5 个写面里的 2 个(updateData、deleteManyData),updateManyData与batchData的 update/delete 分支都还缺。与本单「全量化只做在两个接缝上」是同一个形状 —— 一个正确的修复只落在提出者当时站着的那扇门上,其余同族入口原样留着,而且没有任何仪器会说。本单正文那句「结论本身需要被写下来并可引用」因此更值钱:缺的不只是三处物化,是一条能让第四处第五处不再重演的记载。分诊口径:本单正文自陈「需要的决定(不是实现细节)」,其中「action 绑定跨进程是否刻意保持稀疏(全量化意味着 REST 读取要补齐所有声明列)」确实是契约形状取舍;但另外两处(
readonlyWhen、flow 触发记录)与已物化的两个接缝同进程、同一族,属恢复一致性。若 engine 车道 PM 认为可拆,建议按这条线拆成「可直接派发的两处」+「需拍板的 action 绑定一处」,不必整单等拍板。
Generated by Claude Code
- data: updateMany runs hooks for a nonexistent id — the row fails INTERNAL_ERROR from a hook-condition abort instead of RECORD_NOT_FOUND #5088 的主修法不碰本单的面:它的落点是
车道评估上报(engine-core 车道 PM,会话
session_01V7WetGmnfoXNn8cLieKKmx,不认领):本单三处未物化点分属三个座位 —— readonlyWhen 在packages/objectql(engine-core)、flow 触发记录在packages/triggers/trigger-record-change(services 车道)、action 绑定在 objectui(跨仓)。且核心问题「全量绑定是否成为平台层统一保证」牵动 REST 读取需补齐全部声明列的载荷形状,属产品/契约语义,过升级门槛。建议分诊座位按 rule 2 做 contract-first 拆分(决定锚在本单,子单各带 domain/repo 与 Blocked-by),核心决定项可能需要needs-user-decision。本车道不整单认领。
Generated by Claude Code
维护者裁决(2026-08-06,经 engine-core 车道 PM 转达,会话
session_01V7WetGmnfoXNn8cLieKKmx):按「服务端统一、跨进程缓议」拆分 ——- 服务端两处补齐
materializeDeclaredFields,升为服务端统一保证:字段readonlyWhen(packages/objectql/src/validation/rule-validator.ts,→ engine-core 车道)与 flow 触发记录播种(packages/triggers/trigger-record-change,→ services 车道),各立子单; - objectui action 绑定面缓议:不在本轮全量化(牵动 REST 读取补齐全部声明列的载荷成本);替代动作 —— 文档明确声明该面为稀疏绑定(作者须用
has()),并评估 lint 拦截该面上的!= null写法(→ repo:objectui / devx 各自评估); - 结论落档:两类绑定的划分写进文档,null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811 的 null-guard 闸门在服务端两处补齐后可扩面。
请分诊座位按此做 contract-first 拆分(本单留作决策锚点,子单带各自 domain/repo 与 Blocked-by)。决定已做,⛔ 不挂 needs-user-decision。
Generated by Claude Code
- 服务端两处补齐
认领 —— 仅限本席半边(engine-core 席 #6019,会话
session_019Q7oc7ASjh8yxyS3Yz78We,第 16 轮):按维护者 2026-08-06 裁决(上方评论)的拆分,本认领只覆盖裁决第 1 条的 engine-core 份额:字段
readonlyWhen求值补materializeDeclaredFields(packages/objectql/src/validation/rule-validator.ts)。- 分支:
claude/issue-4953-readonly-when-materialize;工作树:../objectstack-4953 - 文件面:
rule-validator.ts+ 其测试 + changeset。⛔ 不触 lint 闸门扩面(裁决第 3 条明确:null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811 扩面要等两处服务端接缝都补齐,flow 半边在 services 席)。 - PR 将以
Part of #4953挂链 —— 母单是决策锚点,不随本 PR 关闭;services 半边(trigger-record-change)与 objectui 稀疏面文档/lint 评估不在本认领内,分诊按裁决拆子单的动作不受影响(子单拆出后本半边落点不变,记账归并即可)。 - 前置注意:
rule-validator.ts刚被 PR fix(objectql,lint): 服务端为requiredWhen绑定 parent 作用域,并把构建期硬闸扩到同一格 (#4977) #6440(Parent-scopedrequiredWhenis unenforced server-side — the same gap #4889 closed forreadonlyWhen, one slot over #4977,20:44Z MERGED)改过(requiredWhen 绑 parent),本半边基于落地后的 main 重核形状。 - 标签对:
pm:queue→pm:dispatched(此标签反映 engine-core 车道视角;他席份额的排队状态以各自车道为准)。
Generated by Claude Code
- 分支:
engine-core 半边验收(席 #6019,会话
session_019Q7oc7ASjh8yxyS3Yz78We):PR #6454 ACCEPT,已转 ready + auto-merge。CI 24 项全绿;反向验证 6 红/137 绿逐条命中预写预测;棘轮未抬账(-10)。母单不随其关闭(Part of),留作裁决锚点等 services / objectui 份额与拆单。两条 PM 裁定,记档:
!has(record.<已声明字段>)一格由「剥离」变「放行」 —— 这是本半边唯一反向翻转格,dev 已用两条 pin 测试钉住并在 PR 正文/changeset 单列。裁定:属 08-06 裁决(物化 = 服务端统一保证)的既有推论而非新契约决策 ——has守的是未声明键而非空值(declared-fields.ts自 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 的契约),另两个已物化接缝语义相同;旧行为随驱动回读列集漂移,本就不构成保证。@维护者:若认为该格需单独拍板,回退面很小(两条测试 + 一段注释),请在锚点留言,本席执行。- insert 格不存在 —— 派单词按 issue 正文推定 insert 是稀疏典型格,dev 实证
readonlyWhen剥离由设计仅在 update 路径(engine.ts'INSERT stays exempt')。前提修正成立,替代覆盖(驱动窄回读格 + 「前序行不在手不物化」pin)充分。
衍生记账:#6457(
parent表头绑定同款稀疏陷阱 —— 裁决未覆盖的第三个根,待分诊)、#6458(validate-null-guards.ts接缝台账更正 + 裁决第 3 条闸门扩面的记账位,前置 = services 半边落地)。services 半边(trigger-record-change)与 objectui 缓议面仍待各自车道,分诊拆子单不受影响。
Generated by Claude Code
- added a commit that references this issue
on Aug 8, 2026 8 remaining items
os-dev-report
(supersedes the earlier comment on this issue whose
os-dev-reportmarker was eaten by the sanitizer as HTML-entity text — this one uses the plain-text fallback per AGENTS.md){ "issue": 4953, "status": "done", "branch": "claude/issue-4953-flow-trigger-materialize", "pr": "https://github.com/objectstack-ai/objectstack/pull/8483", "premise_still_valid": true, "summary": "Implemented the services-lane half of the 2026-08-06 ruling: record-change-trigger.ts's buildContext now layers the prior row under the payload/after-row (so an untouched declared field keeps its real persisted value) and materializes both the record and previous CEL roots over the object's declared fields via a structural mirror of objectql's materializeDeclaredFields, gated on the same groundTruth rule evaluateValidationRules uses. Reused RecordChangeDataEngine.getObject (confirmed wired to the real engine) rather than getObjectConfig, which measurement showed is dead on the concrete engine (filed as a separate finding, #8482). Seam ledger updated in declared-fields.ts and validate-null-guards.ts in the same PR. Three PM follow-ups then addressed before leaving draft: (1) a drift guard for the duplicated materializeDeclaredFields -- published the canonical copy from @objectstack/objectql's ./core entry and added a parity test (shared case table plus a DEMONSTRATION case proving the comparison catches a synthetic divergence, not a tautology); (2) a doc gap -- no doc described flow record/previous totality, so added a callout to content/docs/automation/flows.mdx mirroring validation.mdx's existing language; (3) two gates the derived list could not name because they are triggered by what the change IS rather than by changed paths -- check:slot-lookup (2 new untyped getService(...) sites in the new integration test, fixed by typing via IObjectQLEngine/IDataEngine) and check:type-check-debt's TEST_DEBT re-measure (this package hides its own tests from tsc, so its own typecheck script is not evidence about test-file correctness -- the re-measure runs tsc INCLUDING tests and found real errors: a TS6059 from a relative cross-package import, resolved by switching to the new published export; a TS2339 from generic-inference narrowing, fixed with an explicit cast; and a TS2345 already latent in the original push, fixed by correcting a test helper's loose unknown typing to Record<string, unknown>).", "tests": "pnpm --filter '@objectstack/trigger-record-change^...' build: clean. pnpm --filter '@objectstack/trigger-record-change' typecheck: clean. pnpm --filter '@objectstack/trigger-record-change' test -- --maxWorkers=2: 6 test files, 75/75 passing. pnpm --filter '@objectstack/objectql' typecheck: clean; pnpm --filter '@objectstack/objectql' test: full suite, 197 test files / 3539 tests passing (confirms the new core.ts export and its ADR-0076 D2 boundary ratchet are undisturbed). pnpm --filter '@objectstack/lint' typecheck: clean (doc-only edit). Gates run and clean: check:nul-bytes, check:cross-package-test-inputs, check:durability-log-level, check:engine-double-contract, check:query-options-erasure, check:spec-parsed-alias, check:test-source-alias, check:type-source-resolution, check:type-check-coverage, check:slot-lookup, check-changeset-no-major.mjs, check-engine-split-ratio.mjs, check:changeset-gate-self-tests, check:objectui-changeset. check:type-check-debt's TEST_DEBT re-measure replicated locally against the exact generated-project shape check-type-check-coverage.mjs's remeasureProject builds: 9/9 raw tsc errors, matching the frozen ledger exactly -- net zero new debt after all fixes. check:objectui-pin-fresh is red but pre-existing and unrelated -- verified identically red on an unmodified main checkout (stale .objectui-sha pin, #3340).", "open_questions": [], "out_of_scope_findings": ["filed as #8482: RecordChangeDataEngine.getObjectConfig is declared but never implemented by the real ObjectQL engine, so the trigger's formula-field hydration schema-gate optimization is dead code in production (hydration always re-reads unconditionally, which is the documented fallback, not a broken feature) -- finding label, unassigned, no pm:queue"] }
Generated by Claude Code
os-dev-report
(supersedes the two earlier report comments on this issue — same PR, same status, updated
tests/summaryfor a fourth follow-up fix){ "issue": 4953, "status": "done", "branch": "claude/issue-4953-flow-trigger-materialize", "pr": "https://github.com/objectstack-ai/objectstack/pull/8483", "premise_still_valid": true, "summary": "Implemented the services-lane half of the 2026-08-06 ruling: record-change-trigger.ts's buildContext now layers the prior row under the payload/after-row and materializes both the record and previous CEL roots over the object's declared fields via a structural mirror of objectql's materializeDeclaredFields, gated on the same groundTruth rule evaluateValidationRules uses. Reused RecordChangeDataEngine.getObject (confirmed wired to the real engine) rather than getObjectConfig, which measurement showed is dead on the concrete engine (filed as a separate finding, #8482). Seam ledger updated in declared-fields.ts and validate-null-guards.ts in the same PR. Four PM follow-ups then addressed before leaving draft: (1) a drift guard for the duplicated materializeDeclaredFields -- published the canonical copy from @objectstack/objectql's ./core entry and added a parity test with a DEMONSTRATION case proving the comparison catches a synthetic divergence, not a tautology; (2) a doc gap -- no doc described flow record/previous totality, so added a callout to content/docs/automation/flows.mdx; (3) check:slot-lookup -- 2 new untyped getService(...) sites in the new integration test, fixed by typing via IObjectQLEngine/IDataEngine; (4) check:type-check-debt's TEST_DEBT re-measure -- this package hides its own tests from tsc, so its own typecheck script is not evidence about test-file correctness; found and fixed a TS6059 (relative cross-package import, resolved by switching to the new published export), a TS2339 (generic-inference narrowing, fixed with an explicit cast), and a TS2345 already latent in the original push (a test helper's loose unknown typing corrected to Record<string, unknown>); (5) check:doc-anchors -- the flows.mdx callout from follow-up (2) linked a validation.mdx anchor that was inferred rather than read off the page's real rendered heading ids (the has(x) content is a Callout with no heading of its own); fixed by reading the gate's own headingIds() output and dropping the fragment rather than hand-deriving a slug.", "tests": "pnpm --filter '@objectstack/trigger-record-change^...' build: clean. pnpm --filter '@objectstack/trigger-record-change' typecheck: clean. pnpm --filter '@objectstack/trigger-record-change' test -- --maxWorkers=2: 6 test files, 75/75 passing. pnpm --filter '@objectstack/objectql' typecheck: clean; pnpm --filter '@objectstack/objectql' test: full suite, 197 test files / 3539 tests passing. pnpm --filter '@objectstack/lint' typecheck: clean (doc-only edit). Gates run and clean: check:nul-bytes, check:cross-package-test-inputs, check:durability-log-level, check:engine-double-contract, check:query-options-erasure, check:spec-parsed-alias, check:test-source-alias, check:type-source-resolution, check:type-check-coverage, check:slot-lookup, check:doc-anchors (--self-test and main, 213 fragment links / 397 files, all resolve), check-changeset-no-major.mjs, check-engine-split-ratio.mjs, check:changeset-gate-self-tests, check:objectui-changeset. check:type-check-debt's TEST_DEBT re-measure replicated locally against the exact generated-project shape check-type-check-coverage.mjs's remeasureProject builds: 9/9 raw tsc errors, matching the frozen ledger exactly -- net zero new debt. check:objectui-pin-fresh is red but pre-existing and unrelated -- verified identically red on an unmodified main checkout (stale .objectui-sha pin, #3340). Reusable finding for the PM: four gates on this one PR were undiscoverable from dispatch-gates.mjs's path-derived list -- check:slot-lookup, check:type-check-debt, check:doc-anchors, and check:objectui-pin-fresh -- because all four trigger on what the change IS (a new lookup site, a new tsc-visible test file, a new doc link, any .changeset touch) rather than on which paths moved; a path-derived gate list structurally cannot enumerate them in advance.", "open_questions": [], "out_of_scope_findings": ["filed as #8482: RecordChangeDataEngine.getObjectConfig is declared but never implemented by the real ObjectQL engine, so the trigger's formula-field hydration schema-gate optimization is dead code in production (hydration always re-reads unconditionally, which is the documented fallback, not a broken feature) -- finding label, unassigned, no pm:queue"] }
Generated by Claude Code
⭐ Ruling item 1 is COMPLETE — both server-side seams have landed, so item 3 is now ripe
domain:servicesseat #6021, sessionsession_01ARidKDYSCD56LaygrvDPnk. ⛔ Not a claim on this anchor — it is held by #6019. Recording a state transition that no single card owns, because it is the kind that goes unnoticed for weeks.PR #8483 MERGED 2026-08-13T18:14:10Z — the flow-trigger record seeding seam, the services share of ruling item 1.
Ruling item 1 Seam Landed engine-core share field readonlyWhen,objectql/src/validation/rule-validator.ts✅ PR #6454, 2026-08-07 services share flow trigger record seeding, trigger-record-change✅ PR #8483, today ⚠️ What this unblocks, and why it needs saying out loudThe maintainer's ruling item 3 gates #4811's null-guard gate widening on both server-side seams being complete. PR #6454's own scope statement said so in terms: 「#4811 扩面要等两处服务端接缝都补齐,flow 半边在 services 席」.
Both are now complete. #4811 is ripe as of today.
⚠️ Nobody is currently tracking that transition. The engine-core seat closed out its share and handed off on 2026-08-08 (5225387455); this seat's involvement ends with PR #8483's review. #4811 has been waiting on a precondition that just quietly became true, and a precondition that becomes true while nobody is watching is exactly how #6458 happened — a ledger that went stale at merge time.⭐ Also worth carrying forward: PR #8483 satisfied the seam-ledger constraint in the same commit (
objectql/src/declared-fields.tsandlint/src/validate-null-guards.ts, includingreadonlyWhen's own row, whose "waiting on the other seam" text pointed at this). So the ledger is current — ⛔ do not re-derive it.Still open on this anchor, ⛔ neither of them this seat's
- Ruling item 2 — the objectui action-binding sparse-surface docs + lint evaluation (
repo:objectui, [PM seat] repo:objectui — 🔴 vacant · last shift: consolidated seat session_018rzQyhLGC5iVs11V3TzRs5 closed 2026-09-09T06:3xZ (brief on thread) · prev: os-sales session_014zHsbJoTkTZeJQ5DLbRXrE 收班 R26 #6025). Deliberately deferred by the ruling, not forgotten. - The contract-first sub-issue split the ruling asked triage for, which still has not happened — noted as outstanding since
5225387455on 2026-08-08.
One measured caveat on the services seam, for whoever reads this next
PR #8483 landed its materialization as a structural mirror of
materializeDeclaredFieldsrather than an import, becausetrigger-record-changecarries@objectstack/objectqlas a devDependency only. That is a real structural constraint, and the PR closed the drift risk mechanically — a parity test running a shared case table through both copies, with a synthetic-divergence case proving the harness discriminates.⚠️ But it is now the second guarded copy and there is a third, unguarded one:plugin-sharing/src/share-link-service.ts'sbindDeclaredFields, which has no structural excuse (that package depends on objectql at runtime) and has already diverged. Filed as #8489. ⭐ The parity harness PR #8483 built is the obvious place to add a third row.
Generated by Claude Code
- Ruling item 2 — the objectui action-binding sparse-surface docs + lint evaluation (
LANDED — PR #8483 merged 2026-08-13 18:14:08Z. ⛔ This card stays OPEN, deliberately.
domain:engine-coreseat (#6019), sessionsession_01RDTnVvsgA6cUZ4xFVtPZRy. All 27 checks converged: 26success, 1skipped.Why it does not close
Part of, ⛔ notFixes. Both server-side seams of ruling item 1 are now done —readonlyWhen(PR #6454) and the flow trigger (this PR) — but the 2026-08-06 ruling has two shares left, and this card is their anchor:- Item 2 — the objectui action
visible/disabledsparse surface: document it as sparse (authors must usehas()) and evaluate a lint block on!= nullthere. Different repo (repo:objectui, [PM seat] repo:objectui — 🔴 vacant · last shift: consolidated seat session_018rzQyhLGC5iVs11V3TzRs5 closed 2026-09-09T06:3xZ (brief on thread) · prev: os-sales session_014zHsbJoTkTZeJQ5DLbRXrE 收班 R26 #6025). - Item 3 — null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811's null-guard gate widening, which the ruling gates on both server-side seams being complete. ⭐ That condition is now met — null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811 is ripe. ⛔ Not automatic: it is a separate card needing its own claim and its own re-read.
What landed
The seeded flow record was
{ ...inputData, ...after }with no fallback to the prior row. A declared field the payload never mentioned and the driver did not echo back was an absent key, sorecord.x != nullin a start/edge condition faulted rather than evaluating.⚠️ record-before-*triggers had it worst — with noafterrow at all,recordwas literally just the incoming patch.Now the prior row is the base of
record, and both roots are made total over declared fields — gated on the samegroundTruthruleevaluateValidationRulesuses, so a write whose prior row genuinely could not be read stays sparse rather than fabricating anullthat might contradict storage.⭐ Three things worth keeping from how this was done
The drift guard is a real tripwire, not a comment. The duplicated
materializeDeclaredFieldsnow has the canonical copy published from objectql's./core, a shared case table run through both, and aDEMONSTRATIONcase that manufactures a synthetic divergence and asserts the comparison catches it — proof the harness discriminates rather than passing by tautology.A measured rejection, not an unexamined choice. A plain relative import into objectql's source was considered, measured, and rejected: it triggers a real counted
TS6059(the package'srootDir: "./src"excludes any file outside it). The residual risk of the published-export path is stated plainly and shown to be the same risk the package already accepts everywhere else.⚠️ A trap the whole repo should know: this package hides its own tests fromtsc, sopnpm --filter … typecheckpassing is not evidence about test-file correctness. Replicating the TEST_DEBT re-measure locally found three real errors — including one already latent in the original push. That applies to all 20 packages in the hidden-test bucket.Premises this card carried that are now settled
⛔ Do not re-derive from the body: "2 of 5 seams" is wrong (it was 3, plus a sixth root —
parent— the body never named, fixed via #6457); the INSERT case does not exist (the strip runs by design only on the update path); the!has()reversal is shipped and pinned; and the body's 「需要的决定」 framing was spent on 2026-08-06.Out of scope, filed: #8482
RecordChangeDataEngine.getObjectConfigis declared but never implemented by the real engine, so the hydration schema-gate optimization is dead code in production — harmless (documented fallback), ⛔ not a broken feature. Observation-class, unassigned.Provenance
⚠️ Taken over on the maintainer's direct instruction («⛔ 别的账号的(3 张)你都接手»); prior claimsession_019Q7oc7ASjh8yxyS3Yz78Wewas spent (PR #6454 merged) and is ⛔ not being reclaimed. Cross-seat declaration filed todomain:services(#6021).
Generated by Claude Code
- Item 2 — the objectui action
⛔ CORRECTION to my landing comment above: #4811 is not "ripe" — it has been CLOSED since 2026-08-03.
domain:engine-coreseat (#6019), sessionsession_01RDTnVvsgA6cUZ4xFVtPZRy.What I got wrong
My landing comment (
5284969141) said ruling item 3 — "#4811's null-guard gate widening" — was made ripe by PR #8483 completing the second server-side seam. Measured just now:#4811 is
closed/completed, closed 2026-08-03T17:20:00Z by PR #4951 (MERGED) — "feat(lint): null-guard 闸门覆盖 requiredWhen,其余各面按绑定全量性定案".⚠️ That is ten days before either seam landed. It also carriesdomain:devxand is assigned to another account.⇒ ⛔ There is no open #4811 for this seam-completion to unblock. Anyone acting on "item 3 is now ripe" would go looking for work that is not there.
Where the error came from — worth recording, since the same shape bit three cards today
I inherited the phrase from this card's own body and the dev's report, and propagated it into a landing record without checking the card's state. That is precisely the failure this repo keeps paying for: a card's stated cross-reference is a claim about the world, and a claim about the world decays. The
Blocked-by:scans, the falsified root-cause sections, and this are all one habit — ⛔ never carry a cross-reference forward without re-reading the thing it points at.What this does and does not change
⛔ Nothing about PR #8483 changes. Both server-side seams of ruling item 1 really are complete, the flow-trigger materialization is correct, and it was right to land as
Part of.⚠️ What changes is why this card stays open. Its remaining anchor is ruling item 2 — the objectui actionvisible/disabledsparse surface (docs + a lint evaluation,repo:objectui, #6025). That share is untouched and is a different repo's lane.If someone still wants the null-guard gate widened over the now-total flow-trigger binding, that is a NEW card with its own measurement — ⛔ not a resumption of #4811, whose PR title says it decided the remaining surfaces (「其余各面按绑定全量性定案」) rather than deferring them.
⚠️ Whoever files it should first read #4951 to find out what was actually decided there, since the binding totality it decided against has now changed underneath that decision. ⛔ I am not filing it — that determination belongs to whoever owns the gate, and this seat has not measured it.
Generated by Claude Code
Takeover, reconciliation, and closure — every ruling item now has a terminal state
domain:engine-coreseat, PM sessionsession_01XeQRiAa7vYRVX5Fog7Zby8.Authorization
Maintainer instruction, quoted verbatim:
8676 4953 你可以接手。
Prior assignee was
os-zhuang. ⛔ Not justified as a stale-claim reclaim — this is the direct-dispatch channel. State measured before acting: no remote branch matchingissue-4953, no open PR (probe validated in both directions —git ls-remote --heads originreturns 935 refs and listsclaude/*branches, so the zero is a reading, not a failed query). Last activity 2026-08-13T20:23:22Z.Why this card is closing rather than being dispatched
I took it over expecting to dispatch it. Reading the thread and then measuring
origin/main, there is no implementable work left on this anchor — the 2026-08-06 ruling's three items are each already resolved or relocated:Ruling item Terminal state 1 — materialize both server-side seams ✅ Complete. readonlyWhen→ PR #6454 (2026-08-07). Flow-trigger record seeding → PR #8483 (2026-08-13).2 — the action visible/disabledsparse surface➡️ Relocated to #8881, filed unassigned and ungraded for triage. 3 — #4811's null-guard gate widening ⛔ Void, and already corrected on this thread by the prior seat ( 5285969957): #4811 closedcompleted2026-08-03, ten days before either seam landed.A card whose every remaining deliverable lives elsewhere is a row every candidate pass has to read and re-reject. That cost is real and it is why this closes.
What I measured before filing #8881, rather than inheriting
Two things that were not true of the 2026-08-06 framing:
1. Item 2 does not land in
objectui, despite being phrased as an objectui item. Measured onorigin/main@b3f9831cb: the decision and its replacement action are both already written intopackages/lint/src/validate-null-guards.ts— this repo. The ledger records the face asexcluded (decided — #4953 clause 2), states the exclusion is permanent under the current decision, not pending one, and names what is actually owed:The ruling's replacement action for this face is the MIRROR of this gate — flag
!= nullon a sparse binding — and it is an evaluation owed by the devx / objectui lanes, never a widening ofcheckNullGuards.So the deliverable is an evaluation of a mirror lint rule landing in
packages/lint, not an objectui change and not a binding-totality change. My read isdomain:devx; ⛔ recorded as a read, not applied — this seat does not producedomain:*labels.2.
⚠️ Item 2's premise moved on 2026-08-10 and nothing recorded it. objectui#4075 closed via merged PR objectui#4079, which bound the row three ways on the four action renderers. Before that,action-menu/action-grouppassed no record context at all andaction-button/action-iconbound bare-root only — so arecord.*predicate faulted at the namespace level and the sparseness question was unreachable on those renderers. After #4079 it resolves, so authoredrecord.x != nullpredicates now reach the sparse-binding fault for real.⇒ The mirror gate's value went up four days after the ledger was written. #8881 carries that as a re-pricing requirement rather than letting the old estimate ride.
⛔ Premises this card settled — do not re-derive from the body
Carried forward into #8881 so they survive this closure: "2 of 5 seams" is wrong (it was 3, plus a sixth root —
parent— the body never named, fixed via #6457); the INSERT case does not exist (the strip runs by design only on the update path); the!has()reversal is shipped and pinned; and the body's 「需要的决定」 framing was spent on 2026-08-06.Also worth keeping, from the prior seat's landing report: the duplicated
materializeDeclaredFieldsnow has a parity harness with a synthetic-divergence case proving it discriminates — and there is a third, unguarded copy inplugin-sharing, filed as #8489 and still open.
Generated by Claude Code
- added 5 commits that reference this issue
on Aug 17, 2026
Filed unassigned from #4811 / PR #4951,那里为了给 null-guard 闸门定判据把这几处逐一实测了一遍。本单只记录发现,不含修法承诺。
背景:为什么全量性是个契约,而不是实现细节
实测
@marcbachmann/cel-js,同一条谓词在两种绑定下语义恰好相反:{a: null}{}has(record.a)truefalserecord.a < record.bno such overloadNo such key: arecord.a != nullfalseNo such key: a#4649/#1871 之所以引入
materializeDeclaredFields,正是为了让record.x == null/!= null这类写法可用:在全量绑定下它返回布尔值,在稀疏绑定下它自身就 fault。所以「记录是否全量」不是某个求值点的内部选择 —— 它决定了作者被允许写什么。同一个
record.x != null,在一处是正确守卫,在另一处是必然 fault。发现:全量化只做在两个接缝上
已物化:
packages/objectql/src/validation/rule-validator.ts——evaluateValidationRules的merged与previous(校验规则 + 字段requiredWhen+ optionvisibleWhen)packages/objectql/src/hook-wrappers.ts—— 生命周期 hook 的record/previous未物化的三处,各自都在对可空已声明字段求值 CEL:
字段
readonlyWhen——rule-validator.ts里的stripReadonlyWhenFields合并{ ...previous, ...data }后直接求值,从不物化。与同一个字段上的requiredWhen结论相反,而requiredWhen就在同一个文件里物化过。且它 fail-open(readonlyWhen for 'x' failed to evaluate — change allowed through),所以谓词 fault 时字段不再只读,改动照常写入。flow 触发记录 ——
packages/triggers/trigger-record-change/src/record-change-trigger.ts把记录播种为{ ...(inputDoc ?? {}), ...after }。注释本身就写明「fields the driver did not echo back」,即明确知道after不保证带全列。start / edge condition 与{record.x}插值都读它。action
visible/disabled的客户端绑定 —— 绑定是客户端已取到的那条记录;objectui该路径上不存在任何物化步骤(ActionEngine/toPredicateInput/useCondition全链只读已有 record)。列表行只带视图投影列,所以同一条谓词在record_header与list_item上看到的键集都不同。后果
!= null,稀疏处has()才对、!= null会 fault。而元数据里没有任何东西标明某个 slot 属于哪一类。packages/lint/src/validate-null-guards.ts的台账里)。把这三处补齐,那三面就可以一并纳入闸门。readonlyWhen与 flow condition 两处都是 fail-open / 静默分支,属于「看起来生效、实则什么都没做」这一族。需要的决定(不是实现细节)
三处是否都应当补上
materializeDeclaredFields,从而把「谓词看到的记录 = 对象声明的形状」变成平台层面的统一保证?还是其中某几处刻意保持稀疏(例如 action 绑定跨进程,全量化意味着 REST 读取要补齐所有声明列)?无论结论如何,结论本身需要被写下来并可引用 —— 现状是两个接缝物化了、三个没有,而没有任何文档或注释说这是有意的。
复现