Skip to content

validate-null-guards.ts 的接缝台账在 PR #6454 合并后会说假话 —— 字段 readonlyWhen 那行仍写着 "sparse / never materializes" #6458

Closed

Description

@baozhoutao

Filed unassigned from PR #6454(#4953 裁决第 1 条的 engine-core 份额)。只记录发现,不含修法承诺 —— 派发口径明确本轮不触 packages/lint(裁决第 3 条:闸门扩面要等两处服务端接缝都补齐,flow 半边在 services 席)。

发现

packages/lint/src/validate-null-guards.ts 的模块头维护着一张接缝台账(#4811 留下的,「一个被排除但没写理由的面,和一个根本没人看过的面无法区分」正是这一族 issue 的主题)。其中一行:

| field `readonlyWhen` | sparse | `stripReadonlyWhenFields` merges `{...previous, ...data}` and never materializes | excluded |

PR #6454 合并后,这一行的 binding 列(sparse)与 evidence 列(never materializes)都不再成立 —— 该接缝的 record / previous 两个根已过 materializeDeclaredFields。verdict 列(excluded)在 flow 半边落地前仍然正确,但理由已经换了:不再是「绑定是稀疏的,规则会开错药方」,而是「裁决第 3 条要求两处服务端接缝都齐才扩面」。

一张说假话的台账比没有台账更糟:它正是下一个作者用来决定「这个面要不要接闸门」的依据,而 evidence 列指向的那行代码已经不在了。

两半,别混在一起

  1. 台账更正(小,随时可做):把 binding / evidence 列改成实际形状,verdict 保持 excluded 但把理由换成「等 flow 触发记录播种补齐」。
  2. 闸门扩面(裁决第 3 条,有前置):两处服务端接缝都补齐后,readonlyWhen 面可纳入 null-guard 闸门。注意这一面的 fail 策略与已覆盖的两面不同 —— 校验规则与 hook condition 是 fail-closed,readonlyWhen 是 fail-open(除 Parent-scoped readonlyWhen is unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 的未绑定根),即「谓词写错 ⇒ 声明的锁被放行」,与 requiredWhen 那行台账已经记下的理由同类(「fail-OPEN,所以一条没守卫的谓词静默地什么都不强制」)。扩面时这条理由要一并写进 evidence 列。

顺带:packages/objectql/src/cel-fault.ts 模块头那句「the same argument that put materializeDeclaredFields in front of both evaluators」在 #6454 后是三处,属同类文档漂移,可一并捎上。

Blocked-by: PR #6454(engine-core 半边)
Blocked-by: #4953 裁决第 1 条的 services 半边(flow 触发记录播种,尚未立单)

#4811 已 closed,故本单不作为它的子单;若分诊按裁决把 #4953 拆成子单,本单可并入「结论落档 + 闸门扩面」那一支。

Activity

  1. added
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    pm:queueReady for the PM dispatch loop
    on Aug 7, 2026
  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Triage: pm:queue + pm:blocked + domain:devx. No target:<major>.

    Classification

    Queue, not finding. Doc-drift cards are usually observation-class, but this one is graded on what the ledger is: validate-null-guards.ts's module-header table is the artefact the next author reads to decide whether a seam gets the gate — it is a decision input, and #4811 exists precisely because "a surface excluded without a reason and a surface nobody ever looked at are indistinguishable". A ledger whose evidence column points at code that no longer exists is worse than an absent row, as the body says. The fix is mechanical, its landing site is a named file and a named table row, and its acceptance is checkable by inspection.

    The body's two halves are correctly separated and only half 1 is in scope for this card:

    1. Ledger correction — rewrite the binding/evidence columns to the shape after fix(objectql): 字段 readonlyWhen 求值补 materializeDeclaredFields —— 服务端第三个接缝 (#4953) #6454; keep verdict: excluded but swap the reason from "the binding is sparse, so the rule would prescribe the wrong fix" to "ruling clause 3 requires both server-side seams before the gate widens". Small, mechanical.
    2. Gate widening — clause 3 of the #4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953 ruling, prerequisite not met (the flow trigger-record seeding half is unfiled). Not this card's work; when it happens, the fail-policy asymmetry the body identifies (validation rules and hook conditions fail closed, readonlyWhen fails open except Parent-scoped readonlyWhen is unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889's unbound root) must go into the evidence column.

    pm:blocked per the body's Blocked-by: lines. Both stand as written: PR #6454 is open and still draft (verified), and the services half of clause 1 (flow trigger-record seeding) has no issue yet. Correcting the ledger before #6454 merges would simply make it lie in the other direction, so the block is real ordering, not caution.

    Routing rationale (landing site, not title vocabulary)

    domain:devx. The fix lands in packages/lint/src/validate-null-guards.ts (module header), and packages/lint ⇒ domain:devx per the skill's domain table. The title's readonlyWhen vocabulary points at engine semantics — this is exactly the case the anchoring rule warns about (#4775): the topic is engine, the file is lint.

    The packages/objectql/src/cel-fault.ts module-header line the body offers as a rider ("in front of both evaluators" becomes three after #6454) sits in domain:engine-core territory. It is one sentence of drift with the same cause, so it is left folded into this card rather than split — but the implementing seat should note that touching it crosses a lane boundary, and if the two-line edit turns into anything more it belongs in a separate domain:engine-core card, not in this one.

    Stale-premise check

    git log --oneline -10 origin/main -- packages/objectql/src/engine.ts shows 9bc846b (#4977 / #6440, requiredWhen parent binding) landed two hours ago — same family, different seam, and it does not disturb the row this card is about. The ledger row quoted in the body is still verbatim on main; PR #6454 has not merged, so the ledger is still telling the truth right now — this card is armed for the moment it stops.

    Release board

    Not target:v17. Internal tooling documentation; nothing on a shipped surface, no user-visible behaviour, and the gate widening it unblocks is itself a ruling-gated follow-up rather than a release obligation.

    Dedup

    Full-text scan of all 455 open issues and PRs across the three repos. No duplicate. Adjacent by shared origin, distinct in landing site: #6457 (the other spin-off from PR #6454 — the parent root's sparse header binding, lands in packages/objectql), PR #6454 itself, #4953 (the ruling). The body's own note that #4811 is closed and so this is not filed as its sub-issue is correct and needs no correction.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 8, 2026
  4. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Claim: PM loop round 6 (devx seat) — unblocked

    Session: session_01F8q5J1MQyocgtNspb15fSn
    Branch: claude/issue-6458-null-guard-ledger-readonlywhen
    Worktree: objectstack-issue-6458
    Domain: domain:devx
    File surface: packages/lint/src/validate-null-guards.ts (the seam ledger) and its tests. ⛔ Not validate-translation-references.ts — that file is claimed in parallel under #6422 this round.
    Container: M — mode:subagent, shared container.

    pm:blocked lifted. The blocker, PR #6454, merged 2026-08-07 22:14 UTC. The premise is not merely unblocked, it is confirmed by the blocker's own body, which states: 「#4811 的 null-guard 闸门扩面按裁决第 3 条要等两处服务端接缝都齐,故本 PR 不触 packages/lint」. So the ledger line was knowingly left behind, and the row for field readonlyWhen — still reading sparse / never materializes — became false the moment #6454 landed.

    ⚠️ The correction is not a one-word swap, and this is the part worth getting right. #6454's own measurement grid shows the change is not uniformly "now materialized": one cell flipped in the opposite direction. !has(record.<declared>) went from true → strips to false → passes — a lock spelled that way stopped locking. A ledger row that now reads "materialized" without recording that reversal would be a second false statement replacing the first. Read #6454's grid and the contract note in packages/objectql/src/declared-fields.ts, and make the row say what is actually true.

    Also check the neighbouring rows while you are in the file: #6454 says two server-side seams were required and it delivered one share. If another row is stale for the same reason, fix it in the same pass and say so; if it is not stale, say that too. ⛔ Do not widen the lint gate itself — that is #4811 and ruling item 3, and it waits for both seams.

    Serial constraints cleared: no open PR touches packages/lint. #6422 is dispatched in parallel this round against a different file in the same package; if you find yourself needing to edit validate-translation-references.ts, stop and report rather than crossing into it.


    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions