You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
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.
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 #7349 — only as a dependent option, and it carries five measured traps:
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.
Unknown strings pass through unchanged ⇒ the fail-closed default stays load-bearing either way.
⭐ canonicalAstOperator does String(op).toLowerCase()first. Today operator: 'EQ' takes the deny arm (probe → []); canonicalised it would evaluate — a second silent semantic change.
⭐ 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.
Carved out of the contract review of PR #7748 (card objectui#7378) by the
domain:uiPM 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:⛔ 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, andDataScopeManageris a third hand-written evaluator vocabulary (the other two being@object-ui/permissions'evaluateConditionand 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
RowLevelFilterregisterScopeWithConfig/setFilters(/applyFilters(hit only the class and its own testRowLevelFilterreferences at base@object-ui/typesDataScope/DataContext(data.ts:1272-1330)DataScopeManagerobjectstackcanonicalAstOperatorfires, 3 files)org:objectstack-aiDataScopeManager5 ·registerScopeWithConfig2 ·RowLevelFilter2 — all in objectui (controlcanonicalAstOperatorfires in both repos, 17)hotcrm@object-ui/corecloudThe options
C — retire under ADR-0049 enforce-or-remove. Closes the operator question for free. Blocked only on the
cloudconsumer read above.Canonicalise through
canonicalAstOperator(@objectstack/spec/data), the way PR #7377 repaired sibling #7349 — only as a dependent option, and it carries five measured traps:eq→=,ne→!=, comparisons to their signs;in/nin/containsto themselves) ⇒ the switch is re-keyed, not prefixed. That is exactly whatValueDataSource.matchesComparisonNode(:105-118) keys on.containssemantics do not survive the transplant.ValueDataSource.ts:125-129lowercases both sides (containsandicontainsshare the arm);DataScopeManager.ts:267isfieldValue.includes(String(filterValue)), case-sensitive. Copying fix(core): teach ValueDataSource's matcher the filter vocabulary the wire already has #7377 would silently widencontainsfor existing rules on a permission boundary. (Related: open core:ValueDataSource'scontainsis case-INSENSITIVE, so it executesicontainssemantics — the spec keeps the two operators deliberately distinct #7379.)canonicalAstOperatordoesString(op).toLowerCase()first. Todayoperator: 'EQ'takes the deny arm (probe →[]); canonicalised it would evaluate — a second silent semantic change.is_empty/isemptyontois_null(AST_OPERATOR_MAP→$null), so emptiness would be answered as null-ness.Plus: 18 of 20
VIEW_FILTER_OPERATORSand 44 of 53VALID_AST_OPERATORSwould move refused → evaluated, needing arms for the null-ness family,between,starts_with/ends_with,not_contains/icontains, and alike/ilikedecision (matchesLikePatternis exported); and the publicRowLevelFilter['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 (
containsfolds case) · #7383 (like/ilikerefused) · ADR-0049.Generated by Claude Code