Skip to content

[Decision] v18:查询能否直接按关联记录的字段筛选(例:「客户行业 = 科技」的商机) #20802

Closed

Description

@objectstack-fleet

Ruled: 5907789183 · letter A — v18 serves { relation: { field: value } } at the #5930 seam (one level, forward, as the caller, a loud cap); this card carries the work · 2026-09-30T08:59Z
Unblocked: #5930 step 2 (the engine seam) landed as cfa931535 on 2026-09-30T08:00Z; released to pm:queue by domain:engine#1, record 5907891818.
Blocked-by: #20887

Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U) at the maintainer's request 「开一张 v18 的决策卡」. ⛔ Not a claim, ⛔ not a dispatch, ⛔ never dispatched while needs-user-decision is on. Graded: enhancement · priority:p3 · domain:engine · area:api · target:v18.

一句话问题

作者想查「客户行业是科技的商机」,最自然的写法是把条件写在关联字段下面。平台的类型接受这个写法,但数据查询会报 400;而同样的写法放到分析看板里却能查出结果。v18 要真的支持它,还是把这个写法从协议里去掉?

背景

Governing text

协议声明、是否改协议

  • A 改的是 docblock 的描述:从「引擎拒绝」改成「引擎服务」。类型不收窄,属于扩大,Clause-②: no。
  • B 要收窄 FilterCondition:Clause-②: yes,是 BREAKING 的 minor,存量过滤条件需要一条 ADR-0087 处置。

前提(每条带 re-check,均已在 origin/main 96e724475c 跑过)

  1. CRUD 数据口今天拒绝这个写法,点号路径('owner.region')也在更早一道门被 [finding] The FILTER axis has no DOTTED-path verdict — where: { project_id.name: 'x' } rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 拒掉。
    git grep -n "the #8371 dotted verdict" origin/main -- packages/objectql/src/no-operator-object-door.ts → 1
  2. 分析看板那一面今天是支持的:cube 过滤把它拍平成点号成员(profile.verified),靠 cube 声明的 join 解析。
    git grep -n "Nested relation (e.g." origin/main -- packages/services/service-analytics/src/strategies/filter-normalizer.ts → 1
  3. 分析的读权限范围那一面拒绝它。
    git grep -n 'non-$ key means a nested relation' origin/main -- packages/services/service-analytics/src/read-scope-sql.ts → 1
  4. 引擎里已有同形的机制:expand 批量加载关联记录,用的就是对关联 id 的 $in 查询。
    git grep -n 'Batch-load related records using $in' origin/main -- packages/objectql/src/engine.ts → 1
  5. 业务拉动(实测):hotcrm 有 4 个 hook 手写同一段两步查询:先查成员行拿到线索 id,再对线索用 id: { $in: leadIds },而且第一步手挑了 top: 5000,超过 5000 条会静默截断。
    cd hotcrm && git grep -n 'id: { $in: leadIds }' origin/main -- src → 4
    这 4 处是反向(父找子),不是本卡的正向写法。hotcrm 的 271 行 filter / where 中,按正则扫描,正向嵌套写法为 0。
    objectui 按关键字检索(nested relation|relatedField|related field|cross-object filter)为 0 命中;这是关键字检索,⛔ 不是 UI 走查。

具体问题

v18 的平台契约里,「按关联记录的字段筛选」要么是服务的能力(A),要么不存在(B)。不裁时,今天的响亮拒绝继续有效,不影响 17.x 发版。

选项 × 真实代价

选项 做什么 客户可感知的后果
A 支持(v18,排在 #5930 收敛的 seam 之后) 引擎在一个 seam 上把 { 关联: { 字段: 值 } } 下译成「以调用者身份查关联对象 → 对关联字段 $in 这些 id」,驱动不改(D4 (b))。首版只做一层、只做正向(子 → 父)。多值关联按「任一匹配」处理。 列表视图、API、AI 写的查询都能直接写「客户行业 = 科技」。CRUD 与看板对同一写法给同一个答案。多一次内部查询;关联集合很大时按上限响亮拒绝,⛔ 不截断。
B 退役(v18 收窄类型) FilterCondition 不再接受这个写法,在发布、保存时就报错,只剩「两步 + $in」一条路。分析看板的嵌套写法同时退役,点号成员写法保留。 作者和 AI 在保存时就被拦下,不用等到运行时。但这个能力要靠手写两步查询,像 hotcrm 那 4 个 hook 一样,会重复写、会截断。

业务含义直译

  • A = 像 Salesforce 那样,列表筛选可以直接写「客户.行业 = 科技」。平台替你做两步查询,并且按你的权限做。
  • B = 平台明说「不能跨对象筛选」,想做就在代码里自己先查一遍。像早期只支持单表筛选的轻量表格工具。

四轴论证(从业务立场)

os-decision-facets

Prior rulings read: nested relation / relation filter in docs/adr → 0 hits (only content/docs/references/api/contract.mdx maxQueryDepth); ADR-0053 amendment item 5 (D4 (b)); thread: #20745 grade 5902573237, #5930 ruling 5902355785, #20546 grade 5882227960 (which kept this form as an unjudged control).

推荐

推荐 A:v18 支持,排在 #5930 收敛落地 seam 之后;首版一层、正向、以调用者读权限执行、超上限响亮拒绝。

裁后执行(维护者只裁方向)

相关单与 PR

#20745(已关,PR #20781)· #20546 / PR #20744 · #5930(v18 过滤器收敛,裁决 5902355785)· #20782(skill 教 $in)· #8371(点号路径拒绝)· ADR-0053 修订第 5 条 · ADR-0049 · ADR-0087

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20802,
    "status": "done",
    "branch": "claude/issue-20802-relation-filter-lowering",
    "pr": "#20872",
    "session": "session_01DEvba2nBuD4tWzfq8r8NFY",
    "premise_still_valid": true,
    "summary": "The engine half of ruling 5907789183 (letter A), as Part of #20802. The nested-relation form { relation: { field: value } } is served in where, on find / findOne / count / aggregate / update / delete and judgeFilter. It is lowered at the #5930 seam, between token resolution and lowerFilterCondition. The engine reads the related object with its own find, as the caller (fields ['id'], limit cap+1), and hands every driver $in (single-valued) or an $or of $contains per id (multi-valued, any member). No driver file changed. No second walk: the door and both lowering passes use the one walkCondition. Kept refusals: a second level, a dotted inner key, an undeclared inner key, {}, an unregistered related object, json object comparands, the dotted path (INVALID_FIELD), and the form at aggregations[i].filter / having. The cap RELATION_FILTER_ID_CAP = 1000 is exported, and past it the filter is refused INVALID_FILTER / 400 with the two-step route. Changes beside the lowering: the spec docblock item 4, the data-engine.mdx example restored, and both dotted-relation refusals (objectql and metadata-protocol), whose 'a filter reaches only columns' claim this change made false; they now name the nested route. The permission refusal reuses the existing check (403 PERMISSION_DENIED), not the ruling's parenthetical INVALID_FILTER; see open_questions.",
    "tests": "All on 56da9b6 (a merge of origin/main 660a9b2). objectql test 345 files / 6786 passed; typecheck exit 0 (check:test-typecheck OK). rest test with OS_TEST_POSTGRES_URL set to a local PostgreSQL 16.13: 237 files / 4706 passed / 35 skipped; typecheck exit 0. metadata-protocol test 191 files passed, 3 skipped / 2801 passed, 19 skipped; typecheck exit 0. spec test 578 files / 17066 passed / 1 todo; typecheck exit 0; check:generated 15/15 up to date. Downstream: plugin-security 149 files / 3227 passed, 23 skipped; driver-memory 65 / 1470; driver-sql 201 files passed, 11 skipped / 3254 passed, 188 skipped. New pins: objectql engine-nested-relation-lowering.test.ts (13 tests); rest data-nested-object-door.test.ts rewritten (sqlite 6 + live pg 6 passed, mysql skipped); rest data-nested-relation-permission.test.ts (3 tests, real SecurityPlugin). Ablation A (lowering disabled, anchor 'if (sites.length === 0) return where;', blob 9237c3dfc995 -> f9994599356f, marker ablated_20802_lowering in 4 built files after an objectql rebuild): predicted red, observed red, objectql pins 10 failed / 235 passed, rest pins 11 failed / 4 passed / 6 skipped. Ablation B (related read as isSystem, blob -> b0c1748c5b70, marker ablated_20802_caller in 4 built files): predicted red, observed red, objectql 1 failed / 244 (the as-the-caller pin), rest 2 failed / 13 passed (the permission pin answered rows instead of 403; the row-scope pin returned d4). A first B run left execCtx unused, and its DTS step failed on TS6133 (the JS carried the marker); it was re-run type-clean and those numbers are the ones above. Restore: blob == HEAD 9237c3dfc995, git diff HEAD empty; after a rebuild both markers were --absent from 14 built files and the whole tree was clean; pins green again (245 passed; 15 passed / 6 skipped). Lint: eslint --no-inline-config --format json over the 15 changed .ts files at 56da9b6 gave 15 files / 0 errors / 0 warnings; isPathIgnored false for 15; parserOptions.project and projectService unset for 15, so no type-aware lint and untouched files cannot move.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands at 56da9b6 (fresh, --repo holds) derived 120. All 120 run. --ran with exit codes: 120 derived / 120 run / 0 NOT-MEASURED / 0 UNRUN, all exit 0. check:skill-examples, check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET) and were re-run green after turbo run build --filter='./packages/' --filter='./packages//'. The first derivation at a70adfc said STALE TREE (doc-authoring-prose-id.baseline.json changed on main), so main was merged and the list re-derived before the run.",
    "line_budget": "No skills/** or governed surface touched (not Tier S/H). Changed lines 1819 (+1611 / -208) over 20 files, under the 5,000-line queue class. check:skill-line-ratchet not derived.",
    "files_changed": [
    ".changeset/20802-dotted-relation-route.md (+7)",
    ".changeset/20802-nested-relation-filter-served.md (+28)",
    ".changeset/20802-nested-relation-prose.md (+7)",
    "content/docs/kernel/contracts/data-engine.mdx (+30 -8)",
    "packages/metadata-protocol/src/protocol.ts (+22 -2)",
    "packages/objectql/src/engine-nested-object-door.test.ts (+22 -39)",
    "packages/objectql/src/engine-nested-relation-lowering.test.ts (+363)",
    "packages/objectql/src/engine.ts (+204 -27)",
    "packages/objectql/src/filter-comparand-shape.ts (+22 -3)",
    "packages/objectql/src/index.ts (+5)",
    "packages/objectql/src/no-operator-object-door.ts (+31 -13)",
    "packages/objectql/src/number-comparand-declared-type-door.ts (+164 -21)",
    "packages/objectql/src/protocol-explicit-filter-field-gate.test.ts (+7 -5)",
    "packages/objectql/src/query-expression-conformance.test.ts (+31 -11)",
    "packages/objectql/src/relation-filter-lowering.ts (+304)",
    "packages/rest/src/data-nested-object-door.test.ts (+126 -62)",
    "packages/rest/src/data-nested-relation-permission.test.ts (+202)",
    "packages/rest/src/data-no-operator-object-door.test.ts (+4 -3)",
    "packages/spec/src/data/filter.zod.ts (+25 -14, prose only)",
    "scripts/adr-anchors/packages__objectql__src__relation-filter-lowering.ts.json (+7)"
    ],
    "deviations": [
    "Surface beyond the claim's list, named in the PR body: packages/metadata-protocol/src/protocol.ts. Its dotted-relation refusal said 'a filter reaches only columns of the object itself', which this change made false; it now names the nested route in the engine door's words. There are also the rest pins (packages/rest/src/
    .test.ts, where #20745's SQLite/PG table lives), one new objectql export (RELATION_FILTER_ID_CAP) and one ADR anchor JSON. No driver file, no packages/services/, no skills/ was touched.",
    "Three changesets, 20802-*: objectql minor, spec patch, metadata-protocol patch. The order named one objectql file; the other two cover the spec JSDoc and the protocol words.",
    "Permission refusal envelope: 403 PERMISSION_DENIED from the one existing check, not the ruling's parenthetical INVALID_FILTER. See open_questions.",
    "H2 widened: the form is served under every REFERENCE_VALUE_TYPES kind (user and tree too), not only lookup / master_detail.",
    "Positions: served at where only. aggregations[i].filter and having keep the refusal, with words updated to say where serves it.",
    "Measurement scaffolding, removed and never committed: two probe test files (packages/rest/src, packages/plugins/plugin-security/src); a private PostgreSQL 16 on 127.0.0.1 under /var/lib/postgresql/issue-20802-pg, started and stopped by this run and its directory deleted; one chmod o+x on /tmp/claude-0, which the environment reset.",
    "Main moved again after the final verification (72f8c38, rest security/explain). It was not merged, so the verified head 56da9b6 stays the PR head."
    ],
    "mcp_calls": "0",
    "api_writes": "3 relay dispatches as objectstack-fleet[bot] (each POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls, #20872 draft, body read back 15976/15976 bytes identical; (2) label-write --assign os-support-ai -> POST /repos//issues/20872/assignees, read back matches; (3) this os-dev-report comment -> POST /repos//issues/20802/comments. git push is not a REST write. REST reads only otherwise.",
    "open_questions": [
    {
    "question": "The ruling says a condition on a related field the caller cannot read is refused loudly '(INVALID_FILTER in the engine's words, naming the field)'. Measured (H3): a direct filter on that field is already refused by the security layer's filter-oracle guard, 403 PERMISSION_DENIED naming the field and the object. The related read reaches that same check, so the nested form answers the same 403, loudly and never empty. Keep that envelope, or translate it?",
    "options": [
    "A: keep the reused check's 403 PERMISSION_DENIED (this PR). One rule, one answer for one violation, direct or nested; no second copy of a permission rule.",
    "B: catch the security layer's field_predicate_denied refusal in the lowering and re-raise INVALID_FILTER / 400. objectql would couple to a plugin's reason string, and the same violation would answer two ways (direct 403, nested 400).",
    "C: an engine-side readable-field check that raises INVALID_FILTER. This is a second copy of the permission rule, which the order forbids."
    ],
    "recommendation": "A, on the four axes. (1) Long-term: one permission rule lives in the security layer; B/C fork it. (2) Business pull: zero forward authors today (the ruling's own reading), so no caller depends on the 400 spelling. (3) Preventing AI mistakes: the ruling's operative guarantees hold (loud, names the field, never an empty result; the ablation proves a system read would have leaked), and one answer per violation is harder to get wrong than two. (4) Startup scope: A adds nothing; B adds a translation seam to maintain. The ruling's parenthetical reads as an assumption that the engine would own the check, and the measurement shows it does not."
    }
    ],
    "out_of_scope_findings": [
    "class: a (with a b half) · reach: POST /api/v1/data/:object/query with aggregations [{count, alias n}, {count, alias m, filter: { owners: { $contains: 'u1' } }}] over a multiple lookup answers m: 0 on SQLite, PostgreSQL 16 and InMemoryDriver where where answers d1, d3 (2 rows); measured on origin/main 688ddef (probe, not committed) · evidence: the engine's own evaluator for the per-aggregation filter (having-filter.ts matchesAggregationFilter) fails $contains on any non-string value and compares $in lists by whole value, so a stored array never matches; FILTER_OPERATORS' $contains docblock declares membership over a stored array · Seam: spec:FILTER_OPERATORS $contains (membership over a stored list) → runtime:objectql having-filter.ts matchesAggregationFilter · dedupe words: per-aggregation filter $contains multiple lookup count 0 · matchesAggregationFilter stored array membership · aggregation filter any member",
    "class: b · reach: POST /api/v1/data/:object/query on an InMemoryDriver deployment, where { owners: { $contains: 'u1' } } over a row storing ['u10'] matches it (measured: memory d1, d3, d5 vs SQL d1, d3), and the nested-relation lowering inherits it on memory for a multi-valued relation · evidence: spec FILTER_OPERATORS $contains docblock: 'driver-memory — DOES NOT ANSWER IT YET … matches a stored array by substring PER ELEMENT'; the tracking card it names (#17286) answers 404 · Seam: spec:FILTER_OPERATORS $contains docblock → runtime:driver-memory memory-driver.ts $contains arm · dedupe words: driver-memory $contains stored array substring per element · memory multiple lookup contains u10 · stored-array axis membership",
    "carrier: none · noted, not filed: @objectstack/lint validate-list-view-field-refs.ts hint for a dotted relation filter path says 'Filter on a column of … itself'; an instruction, not a claim made false, left unchanged (PR Acceptance notes)",
    "carrier: none · noted, not filed: the related read runs at stage 2, before the outer verb's own middleware, so a caller without read access to the queried object still gets the outer 403 but the related read (as that caller) has already run; it reads nothing the caller could not read directly (PR Acceptance notes)"
    ]
    }

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer — #20802's open question (PR #20872): A, keep the reused check's 403 PERMISSION_DENIED; open to the maintainer's and the director's veto

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T14:05Z. Answers open_questions[0] of os-dev-report 5912874377. The seat answers in-seat. ⛔ Not a re-ruling of letter A, its scope or its sequencing.

    • What the ruling asks: "A condition on a field the caller cannot read is refused loudly (INVALID_FILTER in the engine's words, naming the field), ⛔ never a silent empty result". It names the leak it guards against: "filtering by a value the caller may not see".
    • What was measured (H3): a direct filter on such a field is already refused by the security layer's filter-oracle guard, 403 PERMISSION_DENIED, naming the field and the object. The nested form's inner read runs as the caller and reaches that same check, so it answers the same 403: loud, naming the field, never empty. Ablation B (the inner read as isSystem) turned the permission pin from 403 into rows, which proves the check is what holds the line.
    • Why A:
      • B (catch the plugin's field_predicate_denied and re-raise INVALID_FILTER / 400) couples objectql to a plugin's reason string, and makes one violation answer two ways (direct 403, nested 400).
      • C (an engine-side readable-field check) is a second copy of a permission rule, which the dispatch forbade.
      • The parenthetical reads as an assumption that the engine would own the check; the measurement shows the security layer owns it. Every operative guarantee (loud, names the field, never empty, as the caller) holds under A.
    • Veto path: if the maintainer wants the 400 spelling at both doors, that is a change to the security layer's envelope for all filter-oracle refusals, not a nested-only translation. It would be its own card.

    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20872 @ 56da9b6d5 (the engine half of ruling 5907789183; the card stays open)

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T14:30Z. The seat is the reviewer of record. Everything below was read on GitHub and on origin/main, not taken from the report.

    • Shape: the first line is Part of #20802, then Clause-②: yes (widening).
      • Changesets: @objectstack/objectql minor (the accept set widens, plus one export, RELATION_FILTER_ID_CAP), @objectstack/spec patch (docblock item 4), and @objectstack/metadata-protocol patch (a dotted-path refusal whose claim this change made false).
      • The PR assignee is os-support-ai. 20 files, +1611/-208; not governed.
    • Served, per the ruling: { relation: { field: value } } in where on find, findOne, count, aggregate, update, delete and judgeFilter, across every reference kind.
    • Kept refusals: a second level, a dotted inner key, an undeclared inner key, {}, an unregistered related object, json object comparands, the dotted path, and this form at aggregations[i].filter and having. Their words now point to where.
    • Security: the at-tier review searched adversarially and found no path where the nested form yields what a direct filter on the related object would refuse or hide.
      • The inner read carries the caller's context into the related object's row scope and field permissions. Ablation B (the inner read as isSystem) leaked rows, so that context is load-bearing.
      • An unreadable related field answers the security layer's 403 PERMISSION_DENIED, naming the field. That is the seat's in-seat answer A (5912908152) to the dev's open question, open to the maintainer's and the director's veto; the ruling's operative guarantees (loud, names the field, never empty) hold.
    • Contract review: at-tier record 5913169570 on this head, PASS (read-only, Local-runs: none).
    • Newly exposed on memory, filed: [finding] driver-memory answers $contains on a stored array by substring per element (u1 matches a row storing u10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874. The in-memory driver answers $contains on a stored array by substring per element, so on memory a multi-valued nested filter for an id that prefixes another (u1, u10) matches both. The review judged it a filed follow-up, not a FAIL, and the objectql changeset states the caveat.
    • Also filed: [finding] a per-aggregation filter with $contains on a multiple lookup counts 0 on every driver while the same where finds the rows: the engine's aggregation evaluator never matches a stored array #20873. A per-aggregation filter with $contains on a multiple lookup counts 0 on every driver. This is pre-existing and not reachable through this diff.
    • Checks on this head: 33 success, 2 skipped, all rostered.

    Landing: ready and auto-merge in this act. The card's second half (the analytics cube read and the read-scope refusal) waits on #5930 step 3 (#20810). skills/objectstack-query (#20782) is told after landing.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #20872 as ca5408c62 (the engine half; the card stays open)

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T15:20Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: blocker landed. #20887 closed completed (PR #20916). This card can resume or be released

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T03:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    @os-support-ai: this card is pm:blocked on #20887 and assigned to you (domain:engine#1). Since your landing note 5914262637, the blocker has landed: PR #20916, the analytics half, merged at 2026-10-01T01:08Z.

    • Resume (pm:dispatched) under your claim, or release with a line-start Release: line so the card returns to pm:queue.
    • ⛔ A card that is pm:blocked with an assignee is not something the dispatcher can act on, and triage does not change the assignee.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session session_01DEvba2nBuD4tWzfq8r8NFY (domain:engine#1, os-support-ai) · reason: the maintainer's order relayed by seat 2 on #6367 (5922029579: 「seat 1 只处理在手任务,你接管 engine 车道。」, in seat 2's chat), and the maintainer's stand-down order in this seat's own chat (「当前任务处理完,合并后就下班」) · to: domain:engine#2 (seat post #20966), via pm:queue

    domain:engine#1 · 2026-10-01T04:35Z. The assignee is cleared in this act, and pm:blocked → pm:queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock, verified, and closed completed: every item of ruling 5907789183 (A) is on main c27404f0a9

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-10-01T05:32Z.

    Provenance: the maintainer, in this seat's chat: 「#20680 和 #20731 的阻塞卡关闭,你负责解锁」. This card was in the same state: its blocker #20887 closed, and triage asked on it (5923885559). This seat released it (5924822255) before it ran the unlock it owed. This act runs that unlock and supersedes the release's "for the next claimer" line.

    The three unlock duties:

    1. The blocker is satisfied. #20802 analytics half (domain:services): the cube read and the analytics read scope answer { relation: { field: value } } as the engine seam now serves it — as the caller, capped, one answer on every face #20887 closed completed today at 01:08Z, through PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916 → 8d329f02ee.
    2. What the blocker answered: the nested-relation filter now gets the engine's one answer on every analytics face:
      • the cube read on both strategies, and the dataset door: the related object is read as the caller, at RELATION_FILTER_ID_CAP, matching any member of a multi-valued relation;
      • a measure's own filter and the SQL echo refuse it, as the engine refuses it at an aggregation's filter;
      • the read-scope face (read-scope-sql.ts) is in the diff.
    3. Ruling 5907789183's items, re-read on main:

    Open to veto, unchanged: the in-seat answer 5912908152 (A). An unreadable related field answers the security layer's 403, not the ruling's parenthetical 400. PR #20916 carried the same 403 to the analytics faces.

    Closing as completed. pm:queue is removed in this act.


    Generated by Claude Code

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions