Skip to content

#4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953

Description

@xuyushun441-sys

Filed unassigned from #4811 / PR #4951,那里为了给 null-guard 闸门定判据把这几处逐一实测了一遍。本单只记录发现,不含修法承诺。

背景:为什么全量性是个契约,而不是实现细节

实测 @marcbachmann/cel-js,同一条谓词在两种绑定下语义恰好相反:

谓词 全量绑定 {a: null} 稀疏绑定 {}
has(record.a) true false
record.a < record.b FAULT no such overload FAULT No such key: a
record.a != null false FAULT No 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 + option visibleWhen)
  • packages/objectql/src/hook-wrappers.ts —— 生命周期 hook 的 record / previous

未物化的三处,各自都在对可空已声明字段求值 CEL:

  1. 字段 readonlyWhen —— rule-validator.ts 里的 stripReadonlyWhenFields 合并 { ...previous, ...data } 后直接求值,从不物化。与同一个字段上的 requiredWhen 结论相反,而 requiredWhen 就在同一个文件里物化过。且它 fail-open(readonlyWhen for 'x' failed to evaluate — change allowed through),所以谓词 fault 时字段不再只读,改动照常写入。

  2. 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} 插值都读它。

  3. action visible / disabled 的客户端绑定 —— 绑定是客户端已取到的那条记录;objectui 该路径上不存在任何物化步骤(ActionEngine / toPredicateInput / useCondition 全链只读已有 record)。列表行只带视图投影列,所以同一条谓词在 record_header 与 list_item 上看到的键集都不同。

后果

  • 作者无法用一条写法同时正确覆盖两类绑定:全量处要 != null,稀疏处 has() 才对、!= null 会 fault。而元数据里没有任何东西标明某个 slot 属于哪一类。
  • 这直接卡住了 null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811:null-guard 闸门只能接全量绑定的面,上面三处因此被排除(理由与实测表记在 packages/lint/src/validate-null-guards.ts 的台账里)。把这三处补齐,那三面就可以一并纳入闸门。
  • readonlyWhen 与 flow condition 两处都是 fail-open / 静默分支,属于「看起来生效、实则什么都没做」这一族。

需要的决定(不是实现细节)

三处是否都应当补上 materializeDeclaredFields,从而把「谓词看到的记录 = 对象声明的形状」变成平台层面的统一保证?还是其中某几处刻意保持稀疏(例如 action 绑定跨进程,全量化意味着 REST 读取要补齐所有声明列)?

无论结论如何,结论本身需要被写下来并可引用 —— 现状是两个接缝物化了、三个没有,而没有任何文档或注释说这是有意的。

复现

const { evaluate } = require('@marcbachmann/cel-js');
evaluate('record.a != null', { record: { a: null } });  // false
evaluate('record.a != null', { record: {} });           // throws: No such key: a

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    Contributor

    交叉关联(来自 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.」 —— 即谓词看到的记录是否全量,应当是可判定的,而不是靠调用方运气。

    需要说清的是两者的分工,免得被读成同一个修法:

    一条对本单有用的旁证,来自 #5088 的实测:protocol.ts 上 #4435 的「按行诚实」只落在 5 个写面里的 2 个(updateData、deleteManyData),updateManyData 与 batchData 的 update/delete 分支都还缺。与本单「全量化只做在两个接缝上」是同一个形状 —— 一个正确的修复只落在提出者当时站着的那扇门上,其余同族入口原样留着,而且没有任何仪器会说。本单正文那句「结论本身需要被写下来并可引用」因此更值钱:缺的不只是三处物化,是一条能让第四处第五处不再重演的记载。

    分诊口径:本单正文自陈「需要的决定(不是实现细节)」,其中「action 绑定跨进程是否刻意保持稀疏(全量化意味着 REST 读取要补齐所有声明列)」确实是契约形状取舍;但另外两处(readonlyWhen、flow 触发记录)与已物化的两个接缝同进程、同一族,属恢复一致性。若 engine 车道 PM 认为可拆,建议按这条线拆成「可直接派发的两处」+「需拍板的 action 绑定一处」,不必整单等拍板。


    Generated by Claude Code

  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    车道评估上报(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

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    维护者裁决(2026-08-06,经 engine-core 车道 PM 转达,会话 session_01V7WetGmnfoXNn8cLieKKmx):按「服务端统一、跨进程缓议」拆分 ——

    1. 服务端两处补齐 materializeDeclaredFields,升为服务端统一保证:字段 readonlyWhen(packages/objectql/src/validation/rule-validator.ts,→ engine-core 车道)与 flow 触发记录播种(packages/triggers/trigger-record-change,→ services 车道),各立子单;
    2. objectui action 绑定面缓议:不在本轮全量化(牵动 REST 读取补齐全部声明列的载荷成本);替代动作 —— 文档明确声明该面为稀疏绑定(作者须用 has()),并评估 lint 拦截该面上的 != null 写法(→ repo:objectui / devx 各自评估);
    3. 结论落档:两类绑定的划分写进文档,null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811 的 null-guard 闸门在服务端两处补齐后可扩面。

    请分诊座位按此做 contract-first 拆分(本单留作决策锚点,子单带各自 domain/repo 与 Blocked-by)。决定已做,⛔ 不挂 needs-user-decision。


    Generated by Claude Code

  4. self-assigned this
    on Aug 7, 2026
  5. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    认领 —— 仅限本席半边(engine-core 席 #6019,会话 session_019Q7oc7ASjh8yxyS3Yz78We,第 16 轮):

    按维护者 2026-08-06 裁决(上方评论)的拆分,本认领只覆盖裁决第 1 条的 engine-core 份额:字段 readonlyWhen 求值补 materializeDeclaredFields(packages/objectql/src/validation/rule-validator.ts)。


    Generated by Claude Code

  6. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    engine-core 半边验收(席 #6019,会话 session_019Q7oc7ASjh8yxyS3Yz78We):PR #6454 ACCEPT,已转 ready + auto-merge。CI 24 项全绿;反向验证 6 红/137 绿逐条命中预写预测;棘轮未抬账(-10)。母单不随其关闭(Part of),留作裁决锚点等 services / objectui 份额与拆单。

    两条 PM 裁定,记档:

    1. !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 的契约),另两个已物化接缝语义相同;旧行为随驱动回读列集漂移,本就不构成保证。@维护者:若认为该格需单独拍板,回退面很小(两条测试 + 一段注释),请在锚点留言,本席执行。
    2. 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

  7. 8 remaining items

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    os-dev-report

    (supersedes the earlier comment on this issue whose os-dev-report marker 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

  9. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    os-dev-report

    (supersedes the two earlier report comments on this issue — same PR, same status, updated tests/summary for 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

  10. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ⭐ Ruling item 1 is COMPLETE — both server-side seams have landed, so item 3 is now ripe

    domain:services seat #6021, session session_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 loud

    The 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.ts and lint/src/validate-null-guards.ts, including readonlyWhen'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

    One measured caveat on the services seam, for whoever reads this next

    PR #8483 landed its materialization as a structural mirror of materializeDeclaredFields rather than an import, because trigger-record-change carries @objectstack/objectql as 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's bindDeclaredFields, 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

  11. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    LANDED — PR #8483 merged 2026-08-13 18:14:08Z. ⛔ This card stays OPEN, deliberately.

    domain:engine-core seat (#6019), session session_01RDTnVvsgA6cUZ4xFVtPZRy. All 27 checks converged: 26 success, 1 skipped.

    Why it does not close

    Part of, ⛔ not Fixes. 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:

    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, so record.x != null in a start/edge condition faulted rather than evaluating. ⚠️ record-before-* triggers had it worst — with no after row at all, record was 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 same groundTruth rule evaluateValidationRules uses, so a write whose prior row genuinely could not be read stays sparse rather than fabricating a null that might contradict storage.

    ⭐ Three things worth keeping from how this was done

    The drift guard is a real tripwire, not a comment. The duplicated materializeDeclaredFields now has the canonical copy published from objectql's ./core, a shared case table run through both, and a DEMONSTRATION case 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's rootDir: "./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 from tsc, so pnpm --filter … typecheck passing 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.getObjectConfig is 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 claim session_019Q7oc7ASjh8yxyS3Yz78We was spent (PR #6454 merged) and is ⛔ not being reclaimed. Cross-seat declaration filed to domain:services (#6021).


    Generated by Claude Code

  12. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ⛔ CORRECTION to my landing comment above: #4811 is not "ripe" — it has been CLOSED since 2026-08-03.

    domain:engine-core seat (#6019), session session_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 carries domain:devx and 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 action visible/disabled sparse 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

  13. hotlong commented on Aug 15, 2026

    @hotlong
    Contributor

    Takeover, reconciliation, and closure — every ruling item now has a terminal state

    domain:engine-core seat, PM session session_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 matching issue-4953, no open PR (probe validated in both directions — git ls-remote --heads origin returns 935 refs and lists claude/* 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/disabled sparse 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 closed completed 2026-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 on origin/main @ b3f9831cb: the decision and its replacement action are both already written into packages/lint/src/validate-null-guards.ts — this repo. The ledger records the face as excluded (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 != null on a sparse binding — and it is an evaluation owed by the devx / objectui lanes, never a widening of checkNullGuards.

    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 is domain:devx; ⛔ recorded as a read, not applied — this seat does not produce domain:* 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-group passed no record context at all and action-button / action-icon bound bare-root only — so a record.* predicate faulted at the namespace level and the sparseness question was unreachable on those renderers. After #4079 it resolves, so authored record.x != null predicates 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 materializeDeclaredFields now has a parity harness with a synthetic-divergence case proving it discriminates — and there is a third, unguarded copy in plugin-sharing, filed as #8489 and still open.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions