Repository navigation
fix(lint)!: page/view visibleWhen refuses a has() argument that is not a field selection - #25313
Conversation
…t a field selection Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
…ne refuses (ADR-0087 D3) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
Keeps both refused-shape describes in refused-shapes.test.ts. The has() argument detector reads the stack alone and ignores the context parameter #25279 added. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b2148cb74c9f30b0f7251bf32c51d1b6b8e4469c && git checkout b2148cb74c9f30b0f7251bf32c51d1b6b8e4469c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872 && git checkout -B drift-repro 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 && git merge --no-ff 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872
node scripts/docs-audit/affected-docs.mjs --json 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275
|
Contract reviewServed-tier: Reviewed against ruling ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Bill · project thread
Fixes #25281
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Executes ruling
6351260886on #25281, letter A. Draft until the maintainer ratifies that ruling; the seat runs the contract review and the landing.What
CEL defines
has()in one form,has(e.f), withfa field name after a dot. The engine refuses any other argument ashas() invalid argumenton every evaluation, whatever the record holds. The page and viewvisibleWhendoor (validateVisibilityPredicates, soos validate,os build,os lint,defineStackand the page and view save doors) did not judge the argument, sohas(status)andhas(record.items[0])published clean and the console drew the guarded element for every user.@objectstack/lint: a new gating arm,visibility-predicate-has-argument(error), in the existing family, at the same site and path as its siblings. It fires on ahas()call whose single argument is not a.field selection, reports the first such call per predicate, quotes the argument and the engine's wording, and gives one hint line with the spelling the definition admits:has(record.status)or'status' in record, with the root the layer binds (dataon a metadata-editing form); a bare root (has(record)) is toldhas(record.FIELD), neverrecord.record;size(record.items) > 0, or'k' in ewhen the index is a string key;e.?f:has(e.f); anything else: the one form, with the layer root.No null-guard advice is repeated (
validate-null-guardsowns it).os explain visibility-predicate-has-argumentcarries the reasoning.The oracle (mechanism (b) of the ruling): the canonical AST this rule already walks for the lint:
visibility-bare-identifiergoes silent for an identifier that also appears inside ahas()in the same predicate #16118 mask. Ahascall with exactly one argument whose node is not.is refused. AfterrewriteIndexedHas(Formula/CEL: has() over an indexed path is accepted at authoring and refused at runtime, so a visibleWhen using it fails open #25267) the engine refuses exactly those arguments, and the suite pins the equivalence againstcelEngine.compilein both directions: 22 refused shapes and 10 accepted shapes. Mechanism (b) was chosen over (a) because the engine reports one error per source: inbogusFn(1) && has(status)a compile-based helper would see only the unknown function, while the AST walk sees thehas()call too.The lint:
visibility-bare-identifiergoes silent for an identifier that also appears inside ahas()in the same predicate #16118 mask stays load-bearing.has(status)alone yields ONE finding (this one).has(status) && status == 'x'keeps itsvisibility-bare-identifierfinding on the unguardedstatus, beside the new one.@objectstack/spec, ADR-0087 kit: the D3 semantic entryvisibility-predicate-has-argument-refused(step 18) and aREFUSED_STORED_SHAPESdetector.os migrate meta --storedlists a stored page component, form section or section field carrying the shape under that id. No D2 conversion: the test the author meant is not recorded.packages/speccannot parse CEL, so the detector reads the source through a package-internal token reading (conversions/cel-has-argument.ts). A lint-side test (validate-visibility-predicates.stored-listing.test.ts) holds the listing, the page save door andcelEngine.compileto one set over 29 predicates.@objectstack/formula: comment only. The STILL OPEN note abovefirstUndeclaredReferencerecords that the page/view door now reports the argument. The first-error masking it describes is unchanged.Pins rewritten as part of the ruling (not silently)
has()argument shape excepted"), and a new §has()argument shape that quotes the ruling and states what it does not change.validate-visibility-predicates.test.ts: row 3 (has(status)alone), "several barehas()arguments" and the comprehension row flip from[]to the onehas()finding, with no bare-identifier finding. The "lint:validate-visibility-predicatespasses unknown CEL functions clean (thevalidateExpressionpremise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594 narrows this pin by exactly one axis" block becomes two axes and addshas(status)besidetotallyBogusFn(1,2).Unchanged (#13594's scope discipline, verbatim)
has(e.f)for anye, including the indexed and call receivers #25267 lowers; everydynpredicate;type == 'grid',type(record.x) == string,upper(1, 2),1 + 'a'(pinned: nohas()finding); ahas(inside a string literal; a member callx.has(y);has()andhas(a, b), which the engine refuses under another message; the render-side fail-open (ADR-0137 D3).Measured before the first edit
4166f21(shallow clone). The 550has(occurrences hold no bare-name and no index-last argument in a CEL string. The only hits are comment prose ((!has(x) || isBlank(x))in two flow docblocks, one test comment).5801a4f. Everyhas(NAME)/has(e[i])hit is TypeScriptMap/Set.has(, a code comment, CHANGELOG or docs prose.5801a4f:has(status)andhas(record.items[0])on a form field draw no finding fromvalidateVisibilityPredicates, andcelEngine.compileanswershas() invalid argumenton both. The suite at the base pinned that silence (rows 898–926).Tests
Run on this branch, each under
scripts/pm/os-verify-lock.sh:main(headce74b8efc):@objectstack/lint154 files / 7164 tests,@objectstack/formula52 / 1376,@objectstack/spec710 / 21577 (1 todo), all passing; the three packages type-check.main(fix(spec): os migrate meta --stored lists a stored object bound external to a stored or registered managed datasource #25299, head7f0e0e6fe):@objectstack/specsrc/conversions+src/migrations26 files / 1036 tests,@objectstack/lint154 / 7164, both passing; spec and lint type-check. The merge kept both refused-shape describes; the new detector reads the stack alone and does not use the context parameter spec: the stored-shape detector for an external binding on a managed datasource resolves the datasource from the same stack only, so os migrate meta --stored misses a stored object bound to a stored managed datasource #25279 added.validate-visibility-predicates.test.ts›visibility-predicate-has-argument (#25281)(acceptance pair, engine equivalence both ways, prescriptions, mask, first-only, boundaries, carriers);validate-visibility-predicates.stored-listing.test.ts(engine, page door and stored listing over 18 refused + 11 accepted predicates);conversions/cel-has-argument.test.ts; the#25281describe inrefused-shapes.test.ts; two rows and a view-door assertion inruntime-gate.page-visibility-predicate-writes.test.ts.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 92 families, the same list before and after the merge. On head7f0e0e6feall 92 were run and each exited 0;--ranreconciliation: "92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED". This includescheck-empty-changeset,check-adr-0087-registrationandcheck-changeset-no-major. (On the pre-merge headcheck:dual-build-cjs-loadsexited 3 for a missing dist; with the dists built it exits 0.)Dogfood battery
pnpm exec turbo run build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood teston head7f0e0e6fe: 289 files passed, 3 skipped (292); 2491 tests passed, 18 skipped (2509).VERDICT command-exit 0.Acceptance notes
packages/specreads tokens, not the engine's AST. A predicate that is also broken CEL can be listed there while the door names its syntax fault first. Both send the owner to the same predicate.validateExpressionalready refuses both shapes (invalid-cel,has() invalid argument). Its prescription there is the generic "predicates are bare CEL", not ahas()spelling. Noted, not filed: the verdict is right and the surface is outside this card.Generated by Claude Code