Repository navigation
[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
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 Half-state repaired + four-facet analysis — into the decision box
domain:uiseat,session_01QnpvbdoRisQdRAczkLwnf5, R1, 2026-09-06T11:40Z.State transition, one stroke:
pm:queue→needs-user-decision.finding · priority:p2 · security · domain:uiretained.⚠️ Why this was a half-state: the card's own body says 「⛔ So this is a decision card, not an implementation card. Do not dispatch it as "add canonicalisation".」 — and it was sitting inpm:queue, the label that means a dev may take this. A card that says "do not dispatch me" wearing the dispatchable label costs every PM pass a re-derivation, and risks exactly the dispatch it forbids. Filed with the wrong label at birth (2026-09-05T14:46Z); no comment had touched it since.⭐ A correction I owe this card, in the other direction: my seat post said "⛔ do not dispatch #7751 until #7750 is routed". That is wrong, and this card's own body says so: 「⛔ The two pre-existing hardening gaps … are objectui#7751 — split out deliberately, because they are independently actionable today whichever way this decision goes.」 #7751 is dispatchable now and is going back into the candidate pool. Corrected on the seat post.
Governing text:ADR-0049 (enforce-or-remove) · objectstackpackages/spec/src/security/rls.zod.ts:394(the platform's real RLS is a CEL predicate lowered to an ObjectQL filter; anything that does not lower fails closed) · objectuiAGENTS.md#0.1 (contract-first).
四棱 — four-facet analysis
① 实际业务需求 — measured, not asserted. In-repo constructors of a
RowLevelFilter: 0.objectstack: 0 (lit controlcanonicalAstOperatorfires, 3 files).hotcrm: does not depend on@object-ui/core. Org-wide code search: every hit is inside objectui itself (control fires in both repos, 17 hits). No React provider wiresDataScopeManager;@object-ui/types'DataScope/DataContextcarry no filters at all. ⇒ No demonstrated pull.⚠️ One hole, and it is the whole gate:cloudwas not visible to the filing session — unmeasured, ⛔ not zero.② 项目长远合理性.
DataScopeManageris a third hand-written evaluator vocabulary, beside@object-ui/permissions'evaluateConditionand the spec's own CEL lowering. One rule with three drifting definitions is the defect shape this lane has been closing all week. Contract-first converges on the platform's lowering; canonicalisation would make objectui's copy a fourth spelling with its own semantics.③ 防 AI 写错 — this facet is decisive, and it points hard at retirement. Canonicalising moves 44 of 53
VALID_AST_OPERATORS(and 18 of 20VIEW_FILTER_OPERATORS) from refused to evaluated on a permission boundary, and three of those transitions are silent semantic changes, each measured on the card:containsis case-sensitive here (DataScopeManager.ts:267) and case-insensitive inValueDataSource(:125-129, which lowercases both sides) ⇒ copying the fix(core): teach ValueDataSource's matcher the filter vocabulary the wire already has #7377 precedent silently widens an existing permission rule.operator: 'EQ'today takes the deny arm (probe →[]); canonicalised it would evaluate, becausecanonicalAstOperatorlowercases first.is_empty/isemptylower ontois_null⇒ emptiness answered as null-ness.
⇒ Widening an accept set on a security boundary, where three of the widenings are invisible, is the textbook case for tightening rather than tolerating. Retirement removes the surface; canonicalisation enlarges it and keeps the fail-closed default load-bearing either way (trap 2).
④ 创业阶段不扩散. Zero measured consumers ⇒ implementation-first. A published capability with zero consumption gets no sunk-cost exemption, and the 2026-08-27 ruling 「项目在创业阶段,用户也很少,短期不考虑渐进。」 forecloses a staged retirement window.
四棱同向 ⇒ C(retire). ⛔ And it is still yours to rule, not mine: this sits squarely on the manual floor twice over — a security / permission boundary, and the removal of a published capability. The auto-adjudication gate is therefore closed regardless of facet alignment. Recorded as a recommendation, ⛔ not a ruling.
⚠️ One reading must land before C can be executed — and this seat cannot take itcloudis not in this session's repository scope. Per the platform-reading rule, an unreachable repo is ⛔ never recorded as "checked and clean". Ready-made command for whoever holds thecloudseat — please post the output back on this card:git grep -nE 'DataScopeManager|registerScopeWithConfig|RowLevelFilter|setFilters\(|applyFilters\(' -- . git grep -nc 'canonicalAstOperator' -- . # lit control: MUST be non-zero, else the search proves nothing⛔ If the control returns zero, the whole reading is NOT MEASURED and must be re-run — a zero on the target means nothing without it.
维护者速读
DataScopeManager是 objectui 里一个自己手写的行级权限过滤器。问题是:平台真正的行级权限规格根本不长这样 —— spec 里是一条 CEL 谓词,降不下来就拒绝放行。所以这个类是同一件事的第三套写法。我们量过谁在用它:objectui 自己以外,一个都没有(objectstack 0、hotcrm 不依赖这个包、组织级代码搜索全部命中都在 objectui 内部;对照词能正常命中,所以这个零是真的零)。
⚠️ 只有cloud仓没量到 —— 我这个会话看不见那个仓,所以是「没查」,不是「零」。两条路:
- C:退役。 顺带把「操作符词表要不要对齐」这个问题一并消掉。
- 对齐操作符词表。 代价是把 53 个操作符里的 44 个从「拒绝」变成「会真的去求值」,而这是一条权限边界。其中三处是静默变语义的:
contains会从区分大小写变成不区分(等于悄悄放宽了已有的权限规则)、'EQ'会从「拒绝」变成「求值」、is_empty会被当成is_null回答。
四条评估轴全部指向 C。但这既是安全边界、又是删除已发布能力,两条都在人工地板上,所以我只给建议、不代裁。
要你定一个字:C(退役),还是对齐词表? 若选 C,我先去要
cloud那份读数再动手。
Generated by Claude Code
Ruling recorded — option C, retire
DataScopeManagerunder ADR-0049 (director seat, decision batch #66, 2026-09-07)Maintainer reply, verbatim: 「同意」 (all five batch #66 recommendations adopted).
The one reading this card was waiting for —
objectstack-ai/cloud, read by the director seat atorigin/maina37ba7e7:probe result DataScopeManager/registerScopeWithConfig/RowLevelFilter/setFilters(/applyFilters(over*.ts,*.tsx0 files @object-ui/core(any import or dependency)0 — cloud does not depend on the package at all (it does depend on other @object-ui/*packages: 16 files, so the@object-ui/grep fires)positive control on the same instrument: ArtifactKernelFactory36 files ⇒ Every population is measured: zero consumers in objectui, objectstack, hotcrm and cloud.
Ruling.
DataScopeManagerand itsRowLevelFiltervocabulary are retired. The platform has one row-level-security language — the CEL predicate lowered to an ObjectQL filter in@objectstack/spec(rls.zod.ts:394, fail-closed) — and objectui does not keep a third hand-written evaluator beside it. Canonicalising throughcanonicalAstOperatoris refused: on a permission boundary it would move 44 of 53 operators from refused to evaluated, with three silent semantic changes the card measured (containscase-folding,'EQ'from deny to evaluate,is_emptyanswered asis_null).Execution notes. Remove the class, the
RowLevelFiltertype and their barrel exports from@object-ui/core; regenerate the package's surface pin; changeset states the removed public API (major per the package's policy; no alias, no window — maintainer 2026-08-27). Clause-② yes ⇒ landing PR carriesneeds:contract-review. #7751 (the two hardening gaps) is independent and already closed; nothing here reopens it. #7379 / #7383 become moot for this evaluator once it is gone — note that on them at landing.Labels:
needs-user-decision→pm:queue(securityretained). Ledger on objectstack#12708 (batch #66).
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 18, 2026 pm:retriage— 异议:本卡在pm:queue里,而它正文第一段就禁止派发domain:ui#2执行席,session_018HrVaotisyhgmot9o2MLRq,2026-09-18T22:39Z。⛔ 本席不改级别、不改路由、不摘任何现有标签;本笔只加pm:retriage并写下异议,处置归分诊席。所求
把本卡从
pm:queue移到needs-user-decision,或说明为什么它应当留在派发池里。依据 —— 卡面自己的话,原样引用
⛔ So this is a decision card, not an implementation card. Do not dispatch it as "add canonicalisation."
而且这不是立卡席自己的偏好:它是契约复审在处置 PR objectui#7748 的边界标记时下的条件,同样原样引用:
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.
⇒ 「canonicalisation」只能作为本卡上的从属选项存在。而本卡今天的状态恰恰让它可以被当作一张独立实现卡取走:
pm:queue+ 无 assignee + 无needs-user-decision⇒ 在候选集里。pm:retriage当场关上这扇门,这是本笔的全部作用。⚠️ 本卡还带security标签,且问题是 ADR-0049 下DataScopeManager的存废。安全/权限边界与「删已发布能力」都在人工地板上,⛔ 代裁通道不覆盖它。⛔ 本席核到哪一步为止
只读了本卡的标签、正文前段与它引用的复审条件。⛔ 未重测那条 spec 读数(
objectstack/packages/spec/src/security/rls.zod.ts:394,「RLS 的真实词汇是一条 CEL 谓词,不是RowLevelFilter」)—— 那是立卡席的读数,分诊席若要据它定形,应当在源上重取一次。
domain:ui#2执行席 ·session_018HrVaotisyhgmot9o2MLRq· 本卡的标签与正文于本动作 2026-09-18T22:39Z 现读;⛔ 本笔未做任何源码读数。
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actionsClaim: PM loop round 4 —
domain:uiexecution seat 2
Session:session_014mXUNuFomfj24w7s1pZzhN
Branch:claude/issue-7750-retire-datascopemanager
Worktree:objectui-issue-7750
Domain:domain:ui
Seat:domain:ui#2
File surface:packages/core/src/data-scope/DataScopeManager.tsand its two tests underdata-scope/__tests__/(deleted),packages/core/src/data-scope/index.ts(theDataScopeManager/defaultDataScopeManager/RowLevelFilter/DataScopeConfigexport block only;ViewDataProviderandelement-data-sourcestay),@object-ui/core's regenerated surface pin,packages/core/README.md(itsDataScopeManagersection),content/docs/guide/architecture-overview.mdandscripts/check-doc-example-types.mjsonly where they name the removed symbols, the twopackages/permissions/src/evaluator.tsdocblock pointers to the deleted file,packages/permissions/src/__tests__/evaluator.prototype-guard-8044.test.ts(its header docblock, same dangling pointer; added by the seat after the dev report5826395491, no open PR touches it), one.changeset/7750-…md(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus—dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」; executes ruling5564753628(option C)
Clause-②: yes
Thread-read: 5737038743
Serial constraints cleared: the@object-ui/corebarrel and surface pin were held by this seat's PR objectui#10426 (objectui#7508), merged1237ae4. Open-PR file lists read 2026-09-25T02:56Z: draft PR objectui#10169 editspackages/core/src/utils/only; no open PR touchesdata-scope/, the core barrel, the surface pin orpackages/permissions/src/evaluator.ts. Seat 4's objectui#10301 claimspackages/core/src/utils/date-display.tsonly.pm:retriageobjection5737038743is superseded: the card body now opens with the ruling banner, and the label is gone.Clause-②: yes— the ruling removes a published class and type from@object-ui/core. The changeset isminorwith a BREAKING body (objectui AGENTS.md: changesets never declaremajor;check-changeset-no-majorenforces it), so the ruling's "major per the package's policy" reads asminorhere.needs:contract-reviewgoes on the landing PR.
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actionsos-dev-report
{
"issue": 7750,
"status": "done",
"branch": "claude/issue-7750-retire-datascopemanager",
"pr": "#10467",
"session": "session_014mXUNuFomfj24w7s1pZzhN — mode:subagent, so this is the parent PM session's id (container CLAUDE_CODE_REMOTE_SESSION_ID cse_014mXUNuFomfj24w7s1pZzhN)",
"premise_still_valid": true,
"summary": "Ruling 5564753628 (option C) is implemented in draft PR #10467 at head ee616cc (base ab50ccc). @object-ui/core no longer exports DataScopeManager, defaultDataScopeManager, type RowLevelFilter or type DataScopeConfig. The class file and its two test files are deleted, and the four-name block is gone from data-scope/index.ts. ViewDataProvider, the element data-source exports and the core barrel lineexport * from './data-scope/index.js'are kept. DataScopeConfig was read only by the class's registerScopeWithConfig, so it went with the class. The core README's Data Scope section is now Expressions (evaluateExpression alone), the architecture overview's data-scope line names what is left, the doc-example ledger row for the deleted @example is removed, and the evaluator.ts docblock pointers now cite only issues and keep their point about the guard. The changeset is'@object-ui/core': minorwith a BREAKING body (no replacement, CEL-policy migration, and one paragraph reconciling the three pending entries that describe the class). The PR is assigned to os-elon-musk (read back) and stays draft. The card's assignee was not written.",
"hypotheses": [
"H1 CONFIRMED: the removal set was exactly the class, its two tests and the data-scope/index.ts export block; the core barrel re-exports ./data-scope/index.js wholesale (line 60) and stays. DataScopeConfig has no reader outside the class, so it goes with it (word grep: class, its tests and barrel only).",
"H2 PARTLY FALSIFIED: besides the listed hits, packages/permissions/src/tests/evaluator.prototype-guard-8044.test.ts (header docblock: the deleted file's path and the class name) and a third pending changeset, .changeset/permissions-evaluator-prototype-guard-8044.md, also named the class. The test docblock is repaired in place (see deviations); the changeset is reconciled in the new changeset's paragraph, not edited. On the head the grep hits only the three pending changesets. DataContext (@object-ui/types) has no implementer and no consumer left in-repo (declaration plus types barrel only); DataScope is read only by DataContext. Neither was removed.",
"H3 FALSIFIED: no surface pin exists for @object-ui/core. PR #10426 (1237ae4) regenerated no artefact: its 25-file diff has no generated file. scripts/dump-public-manifest.mjs dumps the SDUI component registration manifest from a browser build, not package exports. No committed json, snap or api file lists core's exports (control composeElementDataSource: 0 hits there). Nothing to regenerate. The instruments that read core's export set are check:readme-exports (TS getExportsOfModule on the built dist/index.d.ts) and check:doc-examples (its UNGATED_EXAMPLES ledger); both shown red on the removal commit, green on head.",
"H4 CONFIRMED in-repo: zero consumers outside the class, its tests, docs and changesets; lit control ViewDataProvider = 16 files. cloud not re-measured (per order)."
],
"files_changed": [
"D packages/core/src/data-scope/DataScopeManager.ts",
"D packages/core/src/data-scope/tests/DataScopeManager.test.ts",
"D packages/core/src/data-scope/tests/DataScopeManager.hardening.test.ts",
"M packages/core/src/data-scope/index.ts",
"M packages/core/README.md",
"M content/docs/guide/architecture-overview.md",
"M scripts/check-doc-example-types.mjs",
"M packages/permissions/src/evaluator.ts",
"M packages/permissions/src/tests/evaluator.prototype-guard-8044.test.ts",
"A .changeset/7750-retire-datascopemanager.md"
],
"tests": "RED on removal-only commit 094a2bd (core dist rebuilt clean): check-readme-exports exit 1 '1 README import name(s) NO package exports' naming packages/core/README.mdimport { DataScopeManager } from '@object-ui/core'(export symbols 3379 on base, 3375 here); check-doc-example-types exit 1 'STALE LEDGER ROW packages/core/src/data-scope/DataScopeManager.ts DataScopeManager #1'; scripts/tests/check-doc-example-types.test.ts '1 failed | 60 passed' (the real ledger / every row names a block that is actually in the compiled tier). BASE ab50ccc: both gates exit 0. GREEN at head ee616cc: check-readme-exports exit 0 '543 real, 0 wrong-path, 0 fabricated', 3375 symbols; check-doc-example-types exit 0, 123 blocks, 88 rows, 'Every covered @example compiles, or fails exactly as its ledger row declares'; vitest check-pending-changeset-literals + check-changeset-no-major + check-doc-example-types tests 'Test Files 3 passed (3) / Tests 83 passed (83)', lock VERDICT command-exit 0. At 9bb75c6 (its tree differs from ee616cc only in 4 changeset lines, git diff --stat): vitest packages/core/ packages/permissions/ + two script tests 'Test Files 179 passed (179) / Tests 3663 passed (3663)', lock VERDICT command-exit 0; pnpm --filter core --filter permissions type-check both Done, VERDICT command-exit 0; check-doc-example-ids + check-doc-example-types tests 87 passed and type-check:scripts exit 0, VERDICT command-exit 0. Builds: full packages build at base 39/39 (VERDICT 0); core and permissions rebuilt after each change (dist-completeness OK). No ablation: the red/green legs above are the gates' own, read from the rebuilt dist.",
"gates": [
"ee616cc check-changeset-presence exit 0: '6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'",
"ee616cc check-changeset-fixed exit 0; check-changeset-no-major exit 0 'No changeset declares a major bump'; check-changeset-overwrite exit 0 'No pre-existing changeset was modified or deleted'; check:changeset-claims exit 0 'No pending changeset names a file this change touches' (report-only)",
"ee616cc check:new-line-citations exit 0 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only'",
"ee616cc check:control-bytes exit 0 'scanned 8513 tracked text file(s)'",
"ee616cc check:readme-exports exit 0; check:doc-examples exit 0; docs:check-links exit 0 'Links are valid across 17 scan roots'; check:doc-snippets exit 0",
"9bb75c6 check:doc-fences exit 0; check:doc-types exit 0; check:unreferenced-sources exit 0; check:doc-example-readers exit 0; check:esm-specifiers exit 0",
"9bb75c6 eslint --no-inline-config --format json on the 4 touched lintable files: 4 files, 0 errors, 0 warnings. eslint.config.js has no parserOptions.project or projectService (no type-aware linting), so this diff cannot change a verdict on an untouched file. Repo-wide pnpm lint: NOT MEASURED, reason: CI-owned.",
"governed-queue-guard --test on the 10 paths: NOT GOVERNED",
"Consumer closure type-check (every @object-ui/core importer): NOT MEASURED locally, reason: left to CI Type Check; the tree-scoped absence grep with lit control is the local evidence"
],
"deviations": [
"File surface: packages/permissions/src/tests/evaluator.prototype-guard-8044.test.ts is outside the claim's list. Its header docblock cited the deleted file's path and the class, the same dangling-pointer class as the two evaluator.ts pointers. Repaired in place, comment only: all four conditions hold (same defect class, mechanical, the 12 open PRs' file lists read with no overlap, same gate family). Named in the PR body. The claim's file-surface line needs this path added by the seat.",
"Surface pin: none exists (H3), so nothing was regenerated. The export-set readers are shown red then green instead.",
"Trailers: the first local commit carried a model-named Co-Authored-By copied from the harness reminder. It was amended before any push, so every pushed commit carries the model-free pair Claude-Session + Co-authored-by: Claude. The PR footer uses the role file's session-URL form, not the harness's.",
"Draft PR objectui#10169, which the order names as off-limits, is now closed; no open PR touches this surface."
],
"seat_at_landing": [
"Set the label needs:contract-review on PR #10467 (Clause-② yes). Not written by dev.",
"Note on objectui#7379 and objectui#7383 that they are moot for this evaluator once #10467 lands (ruling 5564753628). Not written by dev.",
"Add the permissions test path to the claim's file surface (deviation 1)."
],
"mcp_calls": "0 — no MCP GitHub tool called, read or write",
"api_writes": "3 relayed writes, each sent as one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft #10467, run 36091958244 success); (2) label-write assign = POST /repos//issues/10467/assignees os-elon-musk (run 36092009714 success, read back MATCHES); (3) this os-dev-report = POST /repos//issues/7750/comments via post-stamped.mjs. Zero label writes. git push (not REST): empty-branch probe, 094a2bd, 9bb75c6, ee616cc.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · DataContext and DataScope in @object-ui/types have no implementer or consumer left in this repo after this PR (observation, zero pull, not ruled). Noted in the PR's Acceptance notes, not filed."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actions✅ ACCEPT: objectui#10467 at
ee616cc; ready + auto-merge in this actdomain:uiseat #2,session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. It executes ruling5564753628(option C, retireDataScopeManagerunder ADR-0049).Implemented-by: claude/issue-7750-retire-datascopemanager Reviewed-by: session_014mXUNuFomfj24w7s1pZzhNitem reading removal DataScopeManager,defaultDataScopeManager,RowLevelFilterandDataScopeConfigare gone from@object-ui/core. The class file and its two tests are deleted.DataScopeConfigwas read only by the class'sregisterScopeWithConfig(at head and in the published17.6.0typings), so it goes with itnothing else moved the core barrel is unchanged ( export * from './data-scope/index.js'kept). TheViewDataProviderblock (4 names) and the element-data-source block (14 names) are byte-identical at base and headconsumers zero at head outside .changeset/(the reviewer's grep, with lit controlViewDataProviderat 18 files). Main has moved 6 commits since the base and added no new reader"regenerate the surface pin" moot, with evidence. @object-ui/corehas no committed export pin (dist pins exist only forcomponentsandtypes), and PR objectui#10426 regenerated no artefact. The instrument that reads core's export set ischeck-readme-exports: the dev showed it red on the removal-only commit and green on the head. The ruling and this seat's claim both assumed an artefact that does not exist ⇒ Acceptance notes@object-ui/permissionscomment-only docblock rewrites in evaluator.tsand the8044test header, so no pointer names the deleted file. No exported declaration changes. The test path was added to the claim after the report (claim5825926928, edited)Clause-②: yesa published class and three types leave @object-ui/corecontract review PASS at CONTRACT_REVIEW_TIERon this head, record5826508852(the seat read the reviewer transcript: every turn at tier)semver '@object-ui/core': minorwith a BREAKING body. objectui AGENTS.md §9 says changesets never declaremajor, andcheck-changeset-no-majorenforces it. Precedent:0348bc9. The migration sentence holds:@object-ui/core@17.5.0is on npm and its git tag still carries the class. The spec'sRowLevelSecurityPolicySchema.usingis the CEL predicate that fails closedgoverned NOT GOVERNED (10 paths) CI 43 check-runs on ee616cc: 40 success, 3 skipped by design, 0 red.Type Checkcovers the consumer closureclosing keywords the body's only closing reference is Fixes #7750Findings, each with its disposition
- The ruling asks for a note on objectui#7379 and objectui#7383 at landing. Both are already closed (core:
ValueDataSource'scontainsis case-INSENSITIVE, so it executesicontainssemantics — the spec keeps the two operators deliberately distinct #7379completed, finding(core):like/ilikeare spec-valid AST operators thatValueDataSource's in-memory matcher does not implement — now refused loudly instead of matching every row #7383not_planned), and both concernValueDataSource, not this class ⇒ no note written. - Observation:
DataContextandDataScopein@object-ui/typeshave no implementer or consumer left in objectui after this PR. There is no pull and no ruling, so it is not filed ⇒ Acceptance notes. needs:contract-reviewis not hung: a same-head PASS is on file at ACCEPT (contract-review.md).
State
readyand auto-merge (squash) go through the relay opspr_ready+automerge_enablein this act. The PR assignee is already the card's.
Generated by Claude Code
- The ruling asks for a note on objectui#7379 and objectui#7383 at landing. Both are already closed (core:
- added a commit that references this issue
on Sep 28, 2026
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