Skip to content

fix(core): DataScopeManager denies a row on an operator it does not implement - #7748

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7378-datascope-unknown-operator
Sep 5, 2026
Merged

fix(core): DataScopeManager denies a row on an operator it does not implement#7748
os-sam merged 1 commit into
mainfrom
claude/issue-7378-datascope-unknown-operator

Conversation

@os-sam

@os-sam os-sam commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7378

DataScopeManager.evaluateFilter implements nine operator spellings and its default arm returned true, so a stored RowLevelFilter carrying any other operator admitted every record the rule existed to hide, with no error and no console line. The arm now returns false, the answer the sibling evaluateCondition in @object-ui/permissions already gives from its own default arm. A red-first test reaches the arm the way stored data does, past the closed union, and pins the fail-closed answer.

Clause-② — accept-set and behaviour changes in this diff

  • packages/core/src/data-scope/DataScopeManager.ts evaluateFilter, default arm: admit (return true) → deny (return false) for any operator outside eq / ne / gt / lt / gte / lte / in / nin / contains.
  • Consequence in applyFilters (unchanged code, changed outcome): a scope holding one unrecognised rule now returns zero rows for that scope, because rules are ANDed; it used to let the remaining rules alone decide.
  • No other accept-set change: the nine implemented spellings behave as before, the declared RowLevelFilter['operator'] union is unchanged, no export was added or removed, nothing is logged or thrown.

Measurements (all taken in this run on origin/main at 565f2b6aa, the branch base)

1. Liveness census — reproduces the execution seat's reading, with one count corrected

probe execution seat (2026-09-02) this run note
files referencing RowLevelFilter (git grep -ln) 2 2 declaration + data-scope/index.ts barrel — reproduced
files referencing DataScopeManager 4 5 the fifth is content/docs/guide/architecture-overview.md:311, a directory-tree comment; present at the card's filing commit d53e472 (checked via REST at that ref), so it was an undercount then, not a new reference — and it is not a producer
in-repo call sites constructing a RowLevelFilter 0 0 git grep -n 'registerScopeWithConfig|setFilters(|applyFilters(' hits only the class itself and its own test
declared union closed, nine members, switch implements all nine yes yes DataScopeManager.ts:29 and the switch at the same nine case labels
lit control: git grep -ln evaluateFilter 1 file the query mechanism fires on a known-present symbol

⇒ Nothing in this repository can reach the default arm through a TypeScript caller. The path that can is scope configuration read from stored or hand-written JSON, where the operator arrives as a plain string — the path the test exercises.

2. Spec-vocabulary gap census — measured on @objectstack/spec 17.2.0 as resolved from packages/core

  • VIEW_FILTER_OPERATORS (@objectstack/spec/ui): 20 members. With an arm: 2 (contains, in). Without an arm: 18equals, not_equals, not_contains, icontains, starts_with, ends_with, greater_than, less_than, greater_than_or_equal, less_than_or_equal, not_in, is_empty, is_not_empty, is_null, is_not_null, before, after, between.
  • VALID_AST_OPERATORS (@objectstack/spec/data): 53 members. With an arm: 9 — exactly the nine implemented spellings. Without an arm: 44 (the canonical words; the seven symbolic forms — equals-sign, bang-equals, the angle-bracket not-equals, greater-than, greater-or-equal, less-than, less-or-equal — spelled in words here because GitHub's sanitizer eats angle-bracket shapes even inside backticks; like / ilike; and the whole null-ness / emptiness family).
  • VIEW_FILTER_OPERATOR_ALIASES maps every implemented abbreviation onto a canonical word the switch has no arm for: eq → equals, ne → not_equals, gt → greater_than, lt → less_than, gte → greater_than_or_equal, lte → less_than_or_equal, nin → not_in.
  • Lit controls for the census query: positive — contains is a member of both published sets (true / true); negative — totally_unknown_op is a member of neither (false / false).

3. The sibling's default arm, quoted, and whether this matches it

packages/permissions/src/evaluator.ts:158-159 reads, verbatim:

    default:
      return false;

It is silent — no console line, no throw, no diagnostic collector. This PR's arm is the same: return false, silent, with a comment. Its own test pins the behaviour as operator: 'unknown' as anytoBe(false) (evaluator.test.ts:229-231); the new test here uses the same shape (as unknown as RowLevelFilter) and explains in its header why the cast is the point. evaluator.ts itself is untouched; it is the control group.

4. Red-first test — measured, not asserted

Base source (565f2b6aa) with the final test text, run via pnpm exec vitest run packages/core/src/data-scope/__tests__/DataScopeManager.test.ts from the repo root:

 Test Files  1 failed (1)
      Tests  9 failed | 19 passed (28)
AssertionError: expected [ { id: 1, status: 'active' }, …(1) ] to deeply equal []
+ Received
+ [ { "id": 1, "status": "active" }, { "id": 2, "status": "inactive" } ]

{ id: 2, status: 'inactive' } — the row the rule existed to exclude — is admitted. All nine new cases fail this way: the two structural cases and the seven published-but-unimplemented spellings (equals, not_equals, greater_than, not_in, starts_with, is_null, is_not_null).

Head (a12751d78), same command: Test Files 1 passed (1) · Tests 28 passed (28).

The red run above was a reverse verification from the committed state: source blob checked out from the base sha, on-disk mutation proven by anchored counts (return true; = 1, Fail closed = 0), restore by git checkout HEAD -- ABSOLUTE_PATH under a trap, proven by git hash-object equal to the HEAD blob 90af27c88… and an empty git diff HEAD, then re-run green.

Every it.each case is chosen so that a correct evaluation of the spelling admits at least one row (numeric age for greater_than): a later change that implements these spellings turns them red rather than passing by coincidence, and the header says the expectation is to be rewritten, never deleted.

Boundary flag — a question this PR does NOT resolve, with the measurements a ruler needs

Should evaluateFilter canonicalise its operator through canonicalAstOperator (@objectstack/spec/data), the way PR #7377 repaired the sibling card objectui#7349, so that the accepted vocabulary is the published one rather than this hand-written nine? The card offers that as a worked precedent, explicitly not a ruling. This PR ships the minimal repair (fail closed) and leaves the question open for needs:contract-review. What a ruler would need, measured:

  • canonicalAstOperator maps the nine implemented spellings to the symbolic formseq to the equals-sign, ne to bang-equals, gt / lt / gte / lte to the greater-than, less-than, greater-or-equal and less-or-equal signs (spelled in words because of the sanitizer), and in / nin / contains to themselves — so canonicalising is not a prefix step, it re-keys the switch onto those symbols (that is what ValueDataSource.matchesComparisonNode keys on today).
  • It passes an unknown string through unchanged (totally_unknown_op → totally_unknown_op), so a fail-closed default arm stays load-bearing under either route.
  • It would move 18 view / 44 AST spellings from refused to evaluated, which needs arms for the null-ness family, between, starts_with / ends_with, not_contains / icontains, and a decision on like / ilike (the precedent refuses those by name).
  • Semantics would not survive a copy-paste: the precedent's contains is ASCII case-insensitive (toLowerCase on both sides); this evaluator's contains is case-sensitive. Canonicalising by copying fix(core): teach ValueDataSource's matcher the filter vocabulary the wire already has #7377 would silently change contains for existing rules.
  • The declared RowLevelFilter['operator'] union would need widening — a public type change on a published package — or the type would keep lying about what the runtime accepts.
  • The execution seat's larger question stands beside this one: with zero in-repo producers, "canonicalise" and "retire under ADR-0049" are competing repairs, and the second is a cross-repo read this seat did not take.

Options a ruler might weigh: A keep the minimal fail-closed arm (this PR) and file canonicalisation as its own card; B canonicalise here (rejected for this PR: widens scope on a p1 security card, and the measurements above show it is a design, not a transplant); C retire the surface (needs the cross-repo consumer read). No recommendation beyond "not in this PR" — that is the contract reviewer's call.

Serial constraints — re-measured at implementation time

11 open PRs; every file list paged to completion (PR #5400 = 633 files across 7 pages, sum verified). Zero touch packages/core/src/data-scope/. The only hits under packages/permissions/ are PR #5400's packages/permissions/CHANGELOG.md and package.json — release bookkeeping, not source, and not in this PR's file surface. Lit control: PR #7621 shows packages/core/src/utils/date-display.ts, as the claim said.

Verification on head a12751d78 (exit codes captured before any pipe; verdict lines quoted)

  • pnpm exec vitest run packages/core/src/data-scope/ (repo root, via the verify lock): Test Files 3 passed (3) · Tests 63 passed (63)VERDICT command-exit 0.
  • pnpm --filter @object-ui/core type-check (after pnpm --filter '@object-ui/core^...' build): echoed tsc --noEmit && tsc -p tsconfig.test.jsonVERDICT command-exit 0. --listFiles proof: tsconfig.test.json reads the edited test file (1 hit) and the build project reads the edited source (1 hit); this is not a typecheck that excluded the test.
  • pnpm --filter @object-ui/core lint — exit 0, ✖ 515 problems (0 errors, 515 warnings). Not a narrowing: this is the full eslint . run CI performs for this package, 214 files by --format json. The 7 + 1 no-explicit-any warnings on the two edited files are identical in count to the BASE blobs linted at the same paths (7 + 1), and 0 any tokens occur on added lines.
  • node scripts/check-changeset-fixed.mjs · check-changeset-no-major.mjs · check-changeset-overwrite.mjs · check-changeset-presence.mjs — all exit 0; presence gate: 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-control-bytes.mjs — exit 0, OK (scanned 6311 tracked text file(s)).
  • Not run locally, left to CI: the repo-wide pnpm lint / pnpm test shards and the rest of the check:* farm.

Changeset

.changeset/core-datascope-unknown-operator-denies-7378.md, @object-ui/core: minor. Argued from the accept-set change: the narrowing is observable on stored data — a deployment holding a rule with an unimplemented spelling sees fewer rows after upgrading — so patch would tell a release reader nothing they can observe changed, which is false on exactly the boundary where being wrong matters. major is CI-refused; per AGENTS.md §版本号策略 objectui's own breaking semantics ship as minor with the break stated in the body, which the body does. The body also carries the liveness reading so a reader can judge whether it bites them today.

Not done here, on purpose

  • packages/core/README.md is unchanged: it documents DataScopeManager without describing operators or the fall-through, so there is no sentence to correct.
  • packages/permissions/src/evaluator.ts is unchanged: it is the control group.
  • No canonicalisation, no new operator arms, no union widening — see the boundary flag.

Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…mplement

`evaluateFilter`'s `default` arm returned `true`, so a stored `RowLevelFilter`
carrying an operator outside the nine the switch implements passed every
record the rule existed to hide — silently. The arm now returns `false`, the
answer `evaluateCondition` in @object-ui/permissions already gives from its
own `default` arm. Red-first test exercises the runtime path stored JSON takes
(cast past the closed union, deliberately) and pins the fail-closed answer for
an operator outside every published vocabulary and for the spec's published
spellings that have no arm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3186.4 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-CefIat3_.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.70KB 116.21KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.27KB 61.22KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.37KB 46.41KB
plugin-dashboard (index.js) 132.87KB 34.68KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Contract review — VERDICT: PASS

domain:ui PM seat, session_01KbJQ1y1J12nZxYzFWhP8Q3. Reviewed in isolation at head a12751d78 by a subagent running as claude-fable-5-1, in a detached scratch worktree since removed (git worktree remove exit 0 without --force, porcelain 0 before removal, stash count 0 unchanged); every ablation under a trap … EXIT INT TERM with the restore proven by object hash; no GitHub writes by the reviewer.

mergeable_state: behind is cosmetic: origin/main is 2 commits ahead (ecf14190e, 81a2eb1fb) and git diff --stat 565f2b6aa origin/main over this PR's files plus evaluator.ts and AGENTS.md is empty — no overlap.

⭐ The finding that reframes the whole area

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. That was not on the card and it changes what the follow-up question even is.

The fix itself is right, and its width is larger than the title

default: return truereturn false, and nothing else moves: the barrel is unchanged, the operator union at :33 is byte-identical in members, no console or throw added — silent, matching the sibling packages/permissions/src/evaluator.ts:158-159 verbatim (grep 'console\.\|throw ' over that file = 0).

⚠️ The consequence the PR pins second is the one to read: applyFilters uses scopeFilters.every (:154-159), so one unrecognised rule denies the whole scope (probe: not_an_operator[], lit control eq id=2[2]).

Census reproduced exactly on @objectstack/spec@17.2.0: VIEW_FILTER_OPERATORS 20 total, 2 with an arm (contains, in), 18 without; VALID_AST_OPERATORS 53 total, 9 with an arm — exactly the nine — 44 without. Lit controls: contains ∈ both, totally_unknown_op ∈ neither. And packages/core/src/index.ts:49 re-exports the whole data-scope barrel, so this is a published surface — Clause-② yes is the right declaration.

Reverse verification: legitimate, and the better shape

The shipped test text is held constant and only the source moves. Re-run in the scratch tree: green 28 passed (28) exit 0 → base source checked out (hash 2189595741…, counts return true;=1 / Fail closed=0) → red exit 1, 9 failed | 19 passed (28), the nine names exactly → restored, hash 90af27c88… = HEAD blob, git diff HEAD 0 lines. Wider data-scope/ suite 3 files, 63 passed. And every it.each case admits ≥1 row under a correct evaluation, so the pin cannot pass by coincidence.

The liveness framing is honest, and one gap is named rather than assumed

In-repo constructors of a RowLevelFilter: 0 (lit control git grep -ln evaluateFilter = 1). objectstack: 0 (control canonicalAstOperator fires, 3 files). Org search: DataScopeManager 5 / registerScopeWithConfig 2 / RowLevelFilter 2 — all in objectui. hotcrm is indexed and does not depend on @object-ui/core.

⚠️ cloud is not visible to that session — ⛔ unmeasured, not zero, and said so rather than rounded to zero. The changeset does not imply a live path; it states plainly that no code in this repository constructs a RowLevelFilter. Defence-in-depth, honestly framed.

Census correction confirmed

git grep -ln DataScopeManager at 565f2b6aa and at the filing commit d53e472 = 5 files, not the execution seat's 4; the fifth is content/docs/guide/architecture-overview.md:311, a directory-tree comment, present at filing, not a producer. The dev's correction is right.

Semver

@object-ui/core: minor — correct under AGENTS.md:239; all four changeset checks green. The body states direction, the AND consequence, the nine implemented spellings, who it reaches, that the spec's canonical spellings are refused not implemented, and the honest liveness reading. A release reader can act: audit stored scope rules for operators outside the nine.

Boundary flag → A, with an ordering constraint I am honouring

The reviewer verified all five of the dev's measurements and added three the dev did not list:

  • contains does not survive a transplant. ValueDataSource.ts:125-129 lowercases both sides; DataScopeManager.ts:267 is includes(String(v)), 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.
  • canonicalAstOperator does String(op).toLowerCase() first — today operator: 'EQ' takes the deny arm (probe → []); canonicalised it would evaluate.
  • ⭐ The spec lowers is_empty/isempty onto is_null, so emptiness would be answered as null-ness.

B refused: a design, not a transplant. A stands — the floor triage set.

Carriers — and one deliberate deviation from the reviewer's instruction

The reviewer asked for one carrier. I filed two, and the split is deliberate:

  • objectui#7750 — the ADR-0049 liveness decision (retire, or canonicalise as a dependent option), carrying the RLS-vocabulary finding, the cloud gap, and all five traps. ⛔ Filed as a decision card, per the reviewer's own ordering constraint: canonicalisation may never be a standalone implementation card.
  • objectui#7751 — the two pre-existing hardening gaps vs the sibling: {field:'constructor', operator:'ne'} admits all three rows where the sibling refuses prototype keys and uses hasOwnProperty; and age gte 0 admits null and '10' where the sibling requires typeof number on both sides.

Why split: #7751 is independently actionable today whichever way #7750 goes, and it has the same failure direction as this card — admit, silently, on the same boundary. Burying a live hardening gap inside a retire-or-canonicalise decision is how it sits unfixed while the decision stalls. ⛔ Neither of them lives only in this PR body.

Both #7379 (contains folds case) and #7383 (like/ilike refused) verified open — correctly not re-filed.

Non-blocking, not gating merge

  1. The changeset states the width qualitatively with six named examples; the counts (18 of 20 view / 44 of 53 AST) live only in the PR body and test header. Recorded on [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.
  2. Cross-repo reading recorded above and on [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: objectstack 0, hotcrm 0 (indexed, no @object-ui/core dependency), cloud unmeasured.

Coverage limit stated rather than papered over: the reviewer could not read the individual shard log (three routes blocked), so which of the four green shards executed the new file is unmeasured — the execution chain is established (vitest list --filesOnly includes the file; ci.yml:704 shards that set; all four shards green), but the shard identity is not.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 5, 2026 14:48
@os-sam
os-sam added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 83c77dc Sep 5, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-7378-datascope-unknown-operator branch September 5, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: DataScopeManager.evaluateFilter returns true for an unknown operator — an unrecognised ROW-LEVEL scope rule widens what a user can see, silently

2 participants