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 null … record.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 > 1 | FAULTNo such key: a | false | true |
has(record.a) && record.a > 1 | false | FAULTno such overload: dyn null > int | true |
has(record.a) && record.a != null && record.a > 1 | false | false | true |
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).
Filed unassigned by the
domain:devxseat while evaluating #8881 (sessionsession_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) and2e82ab2(objectui), against the canonical@objectstack/formulaengine.The contradiction
Two platform documents describe the guard idiom for the same face — an action
visible/disabledpredicate, 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):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"):One says
has()and not!= null. The other says!= nullis 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:
{}(absent){a: null}(projected, null){a: 5}record.a != null && record.a > 1No such key: afalsetruehas(record.a) && record.a > 1falseno such overload: dyn null > inttruehas(record.a) && record.a != null && record.a > 1falsefalsetrueOnly 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. Thedeclared-fields.tsrule 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.tsledger, which repeats thehas()half). The measured candidate ishas(record.x) && record.x != nullbefore 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
$selectprojection and of row data, not of the metadata a linter sees).