Skip to content

The sparse action visible/disabled face is given two contradictory guard idioms by two platform documents, and measured, neither one alone is correct #8975

Description

@os-project-manager

Filed unassigned by the domain:devx seat while evaluating #8881 (session session_011RB4waLuNbdruCo6X9oobm). Out of scope there — that card is an evaluation of a lint gate; this changes what authors are told to write, which is a contract question. Backlink: #8881.

Measured on origin/main @ 585edf738 (objectstack) and 2e82ab2 (objectui), against the canonical @objectstack/formula engine.

The contradiction

Two platform documents describe the guard idiom for the same face — an action visible/disabled predicate, whose record binding is sparse (a list row carries only the view's projected columns; no materialization step exists on that path).

packages/objectql/src/declared-fields.ts (the engine-side contract):

an author on that surface guards with has(), not != null.

examples/app-showcase/src/ui/actions/predicate-matrix.action.ts (the browser-measured live fixture, under a heading that reads "Authoring rules these specimens encode — measured against this runtime's CEL engine, not assumed"):

Null-guard before you traverse or call.record.f_json.nested.k and record.f_tags.size() FAULT on a record where the field is nullrecord.f_json != null && record.f_json.nested.k == "v" is the portable form

One says has() and not != null. The other says != null is the portable form. Both are on the sparse action face.

Measured: each is individually insufficient, because two failure modes are live at once

A list row can omit a column (absent key) and can carry a projected column holding NULL (null value). The two idioms each cover one:

predicate{} (absent){a: null} (projected, null){a: 5}
record.a != null && record.a > 1FAULTNo such key: afalsetrue
has(record.a) && record.a > 1falseFAULTno such overload: dyn null > inttrue
has(record.a) && record.a != null && record.a > 1falsefalsetrue

Only the conjunction is safe across all three bindings, and neither document states it. A fault here is fail-closed — the action silently vanishes, indistinguishable from the gate having said no.

Adoption is zero

Census of authored (non-test) record-scoped action predicates across both repos: 34 predicates, 0 of which use has(), including every platform object (sys-user, sys-invitation, sys-member, sys-approval-request) and the CRM/showcase examples. The declared-fields.ts rule has no uptake anywhere in the platform, and the showcase actively teaches the other idiom.

What is owed

Decide the one correct sentence for this face and state it in both places (and in the validate-null-guards.ts ledger, which repeats the has() half). The measured candidate is has(record.x) && record.x != null before any traversal, call, or ordering/arithmetic use.

⛔ Not claimed here: that the binding should become total. That is refused permanently by #4953 clause 2 and is not re-opened by this finding — this is only about the guidance for the face as it stands.

⛔ Not claimed here: that a lint rule should enforce it. #8881 evaluated and declined the mirror gate on measured grounds (sparseness is a property of the view's $select projection and of row data, not of the metadata a linter sees).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions