Skip to content

[Decision] DataScopeManager under ADR-0049: retire it, or canonicalise its operator vocabulary — the spec's real RLS vocabulary is a CEL predicate, not a RowLevelFilter #7750

Description

@claude

Carved out of the contract review of PR #7748 (card objectui#7378) by the domain:ui PM seat. The reviewer made filing this a condition on its disposition of that PR's boundary flag, and named an ordering constraint that is the whole point of this card:

The follow-up must be filed as the ADR-0049 liveness decision, with canonicalisation only as a dependent option on that card, never as a standalone implementation card.

⛔ So this is a decision card, not an implementation card. Do not dispatch it as "add canonicalisation".

⭐ The finding that reframes the question

objectstack/packages/spec/src/security/rls.zod.ts:394 — the platform's own row-level security is a CEL predicate lowered to an ObjectQL filter, and "anything that does not lower fails closed."

⇒ The spec's real RLS vocabulary is not RowLevelFilter-shaped at all, and DataScopeManager is a third hand-written evaluator vocabulary (the other two being @object-ui/permissions' evaluateCondition and the spec's own lowering). That is the context any answer here needs, and it was not on #7378.

Liveness, as measured for PR #7748

probe reading
in-repo constructors of a RowLevelFilter 0registerScopeWithConfig / setFilters( / applyFilters( hit only the class and its own test
RowLevelFilter references at base 2 files (declaration + barrel)
@object-ui/types DataScope / DataContext (data.ts:1272-1330) carry no filters
React provider wiring DataScopeManager none
sibling objectstack 0 (lit control canonicalAstOperator fires, 3 files)
org code search org:objectstack-ai DataScopeManager 5 · registerScopeWithConfig 2 · RowLevelFilter 2 — all in objectui (control canonicalAstOperator fires in both repos, 17)
hotcrm indexed, and does not depend on @object-ui/core
⚠️ cloud NOT visible to that session — unmeasured, not zero. Reading it is the first task of this card.

The options

C — retire under ADR-0049 enforce-or-remove. Closes the operator question for free. Blocked only on the cloud consumer read above.

Canonicalise through canonicalAstOperator (@objectstack/spec/data), the way PR #7377 repaired sibling #7349only as a dependent option, and it carries five measured traps:

  1. It maps the nine implemented spellings to symbolic forms (eq=, ne!=, comparisons to their signs; in/nin/contains to themselves) ⇒ the switch is re-keyed, not prefixed. That is exactly what ValueDataSource.matchesComparisonNode (:105-118) keys on.
  2. Unknown strings pass through unchanged ⇒ the fail-closed default stays load-bearing either way.
  3. contains semantics do not survive the transplant. ValueDataSource.ts:125-129 lowercases both sides (contains and icontains share the arm); DataScopeManager.ts:267 is fieldValue.includes(String(filterValue)), case-sensitive. Copying fix(core): teach ValueDataSource's matcher the filter vocabulary the wire already has #7377 would silently widen contains for existing rules on a permission boundary. (Related: open core: ValueDataSource's contains is case-INSENSITIVE, so it executes icontains semantics — the spec keeps the two operators deliberately distinct #7379.)
  4. canonicalAstOperator does String(op).toLowerCase() first. Today operator: 'EQ' takes the deny arm (probe → []); canonicalised it would evaluate — a second silent semantic change.
  5. ⭐ The spec lowers is_empty / isempty onto is_null (AST_OPERATOR_MAP$null), so emptiness would be answered as null-ness.

Plus: 18 of 20 VIEW_FILTER_OPERATORS and 44 of 53 VALID_AST_OPERATORS would move refused → evaluated, needing arms for the null-ness family, between, starts_with/ends_with, not_contains/icontains, and a like/ilike decision (matchesLikePattern is exported); and the public RowLevelFilter['operator'] union would have to widen.

Not in scope

⛔ The two pre-existing hardening gaps vs the sibling evaluator are objectui#7751 — split out deliberately, because they are independently actionable today whichever way this decision goes.

Refs: objectui#7378 · PR #7748 · #7349 / PR #7377 (the precedent) · #7379 (contains folds case) · #7383 (like/ilike refused) · ADR-0049.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpm:queuepriority:p2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions