…only an unlowerable one (objectui#13082)
Studio's RLS editor asked `@objectstack/lint`'s
`validateRlsPredicateEnforceability` only for a clause
`isSupportedRlsExpression` refuses. A lowerable predicate the rule
refuses on what it reads, such as `current_user.nope == owner_id`
(`rls-predicate-unknown-user-variable`), linted clean and autosaved.
`rlsPredicateVerdict` now asks the rule for every USING / CHECK clause
that parses, on the same one-clause stack, and carries each finding's
severity and message as written. The verdicts that read the clause
alone reach the editor; the ones that read the object's field map, its
field types or the host engine stay at publish, for the reasons the
docblock gives.
Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #13082
Clause-②: no
What changed
Studio's RLS policy editor now asks
@objectstack/lint'svalidateRlsPredicateEnforceabilityabout everyUSING/CHECKclause that parses, not only about one the runtime cannot lower. Before,rlsLowerabilityVerdictincelAuthoring.tsreturned nothing as soon asisSupportedRlsExpressionaccepted the clause, so the rule's reference passes never ran in the editor. A lowerable predicate the publish door refuses, the card'scurrent_user.nope == owner_id, linted clean and autosaved.rlsPredicateVerdict(renamed fromrlsLowerabilityVerdict) hands the rule the same one-clause stack as before and carries back each finding's severity and message as written. No sentence is written in objectui, and nothing is synthesized: what the rule does not say, the editor does not say. For the card's predicate the editor now shows the rule'srls-predicate-unknown-user-variablemessage, which namescurrent_user.nopeas a value nothing pre-resolves and ends "so the policy is dropped and grants no access" (CHECK: "admits no write"). It is anerror, so Save and the package-door autosave hold.isSupportedRlsExpressionmember of the feature-detectedFormulaModuleshape: the pre-check was its only reader.celAuthoring.tsdocblocks, twoCelPredicateField.tsxcomments and onepackages/app-shell/README.mdbullet now describe the rule's verdicts, not lowerability alone.@object-ui/app-shellpatch.Landing point as claimed:
packages/app-shell/src/views/metadata-admin/celAuthoring.ts. No hint plumbing moved: no pass the editor can judge needs more than the clause (see the table below). Outside the claim's file list, as comments and docs only: the twoCelPredicateField.tsxcomments and the README bullet, which would otherwise describe only the shape pass (AGENTS.md commandment 2). No export, prop or published key moves;celAuthoring.tsstays internal to@object-ui/app-shell.First act: the reproduction, through the real editor path
Per the seat's verification route, the reproduction runs through the real editor path with the real
@objectstack/lintrule at objectui's pin (f4755a68a797):lintCelPredicate, andPermissionAdvancedFacetsmountingCelPredicateFieldand reporting blocking errors toPermissionMatrixEditor's Save gate. On base6058bf8the new pins fail exactly where the card says:lintCelPredicate('current_user.nope == owner_id', { clause: 'using' })answered[](CHECK the same), while the rule on the same one-clause stack answers onerls-predicate-unknown-user-variablefinding aterror.PermissionAdvancedFacetsreported0blocking errors up for the predicate in USING, in CHECK and in both (expected 1, 1, 2), so Save stayed open.Run on base:
Tests 13 failed | 15 passed (28)over the two pin files. A live Studio boot was not needed: the editor path showed the silence. NOT MEASURED: the live publish 422 on a running server, reason: the rule the editor now asks is the function the runtime publish gate runs (runtime-authoring-gate.tspasses the shared rules, this one included), so the door's verdict was read from the function.Every pass of the rule, and where each lives now
Read from
packages/lint/src/validate-rls-predicate-enforceability.tsat the pin, and probed through the installed rule.rls-predicate-unenforceable,-unparseable,-over-budget, error)current_usermember nothing pre-resolves, in a scalar position (rls-predicate-unknown-user-variable, error)current_uservalue of the wrong type for its position: a set under==, one value or the whole context on the right ofin(rls-predicate-unenforceable, error)nulllist member or ordering bound (rls-predicate-unenforceable, error)status in [null]);status in ['a', null]andamount > nullare already the engine's blocking type error, and the rule is asked only once a clause parsesrls-predicate-unknown-field, error)rls-predicate-unenforceable, error)rls-predicate-unenforceable, error)rls-predicate-unenforceable, error)judgeFilterMechanism hypotheses, measured
isSupportedRlsExpressionaccepts, the reference pass; every id and what it reads is in the table. All severities areerrorat the pin.nope_field == current_user.idgetsrls-predicate-unknown-field. The type arms stay silent on names only (status != amount,status != tags: no finding), and an empty field map gives no finding. The partial case is falsified. The editor's list comes from the host'sloadObjectFields, which readsclient.get('object', name)without the draft overlay (useMetadataClientsetspreviewDraftsonly inside?preview=draft). A field added in a pending object draft is therefore missing:region == 'EU'against that list answersrls-predicate-unknown-field, and against the universe the package publish judges (draft folded in) answers nothing. That is a false error holding Save on a predicate the door accepts, so the object is not fed to the stack and the pass stays at publish. The PM's suggested route (feed the object when known) is replaced by this, as its own condition required.current_user.nope == owner_idin both clauses, and it keeps its own silence on an app-staged membership key (owner_id in current_user.team_ids: no finding, pinned).owner_id == current_user.idis clean on both clauses and reports 0 blocking errors up; the objectui#13061 shape tests pass unchanged (celAuthoring.test.ts,PermissionAdvancedFacets.cel.test.tsx).Tests (head
6cd39a5)New
celAuthoring.rlsReferencePasses-13082.test.ts. Every expected finding is read from the published rule at call time; no message is written in the test:current_user.nope == owner_idin USING and in CHECK lints to exactly the rule'srls-predicate-unknown-user-variableerror;owner_id == current_user.idstays clean in both.owner_id == current_user.org_user_ids,owner_id in current_user.id,status in current_userandstatus in [null], both clauses, each the rule'srls-predicate-unenforceableerror. Also pinned: the engine refuses the mixednulllist and thenullordering bound first, with exactly one blocking error; and the app-staged key stays silent.undefined,[]) gives no error; the two type arms are refused by the rule over declared types and give no editor error.PermissionAdvancedFacets.cel.test.tsx: the card's predicate in USING, in CHECK and in both reports 1, 1 and 2 blocking errors up; the control on both clauses reports 0 with both editors showing the valid affordance.Run from the worktree root under the verify lock:
pnpm exec vitest run --maxWorkers=2over the 33celAuthoring.*,CelPredicateField.*,PermissionAdvancedFacets.*andPermissionMatrixEditor.*test files underpackages/app-shell/src/views/metadata-admin/. Result:Test Files 33 passed (33),Tests 310 passed (310), lock verdictcommand-exit 0. The full app-shell suite is CI's.Ablation (one-off, not kept)
Mutation via
scripts/ablation-replace.mjs(objectstack checkout): put the objectui#13061 gate back at the top ofrlsPredicateVerdict(return nothing when the engine'sisSupportedRlsExpressionaccepts the clause), that is, shape-pass-only again. Anchorx1 -> x0, replacementx0 -> x1, blobf9df94a897bc -> 017f941d05af. Over the new file,PermissionAdvancedFacets.cel.test.tsxandcelAuthoring.test.ts:Tests 13 failed | 57 passed (70). Red: the ten surfaced-verdict pins and the three facets blocking pins. Green as expected: both controls, the app-staged key, the shape pin, the two engine-first pins, the four left-to-publish pins and every objectui#13061 test. Restored by the tool: blobf9df94a897bcequals HEAD,git diff HEADempty. A first attempt was refused by the tool before it ran anything (its replacement contained its anchor, so the anchor count could not drop); nothing ran under it. Nodist/is involved: the tests import./celAuthoringfrom source.Gates (local, head
6cd39a5)pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' run build(closure)pnpm --filter @object-ui/app-shell type-check(both projects;--listFilesOnlyshowstsconfig.test.jsonreads both test files)pnpm --filter @object-ui/app-shell lintCelPredicateField.tsx, is on untouched code)pnpm check:new-line-citations0 new citation(s))pnpm check:control-bytespnpm check:changeset-claims/check:pending-changeset-literalsnode scripts/check-changeset-presence.mjs/check-changeset-no-major.mjspnpm check:test-path-roots/check:vi-mock-specifiers/check:vi-mock-inherit/check:vi-mock-override-shape/check:unreferenced-sourcesnode scripts/check-governed-queue-guard.mjs --testover the diffNOT GOVERNED)pnpm check:readme-exportstype entry ./dist/index.d.ts is not on diskfor app-shell itself and six other unbuilt packages). The README hunk is a prose bullet with no fenced block; CI builds and runs it.No i18n key moves, so the i18n checks were not run.
dispatch-gates.mjsrefuses objectui, so this list was derived by hand from the diff.Acceptance notes
loadObjectFieldsinPermissionMatrixEditor.tsx) reads the published object, while authoring surfaces elsewhere read the draft-overlaid one (useObjectFields, objectui#11895). Even with the overlay, the single-item publish door and the package batch see different universes. Making the editor judge unknown fields at error therefore needs a decision on which universe the RLS editor's field list should mirror. That is not this card's. Today an unknown field gets at most the engine's non-blocking near-miss warning, and publish refuses it.packageScope). The editor judges the clause alone, so no scope question arises for the verdicts it now carries.Generated by Claude Code