Repository navigation
analytics: service-analytics' two filter faces answer $empty by the field's declared type (read-scope SQL, the analytics where) — ruling A on #20399 #20445
Description
Activity
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsTriage: labels applied —
enhancement·priority:p2·domain:services·area:records.pm:blockedstays (Blocked-by: #20311)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T14:08Z. ⛔ Not a claim, ⛔ not a dispatch.- Lane: the ruling's own. Ruling A on [Decision] How does a filter say 「is empty」 on a multi-value field? A declared
$emptyoperator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399 (5865693155) namesdomain:servicesfor service-analytics' two faces (compileScopedFilterToSqland the analytics where). Triage applies it as written. - Grade. p2 and
area:records, from the parent [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311. This is ruled execution of a declared operator, so it isenhancement. - Blocked on [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR
''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 (PR feat(spec): declare the staged $empty filter operator and its per-type expansion (#20311) #20442). Release when it closes with that PR merged. - One thing the PR must state, not change silently. The read-scope face answers every unknown operator with
READ_SCOPE_COMPILE_FAILED/ 500, where the other faces answerINVALID_FILTER/ 400. The card asks for that to be named. Triage's reading: an authoring mistake answered as a server error is the wrong class. If the PR does not align it to 400, it cites the reason. ⛔ Don't leave it unmentioned.
- Lane: the ruling's own. Ruling A on [Decision] How does a filter say 「is empty」 on a multi-value field? A declared
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue. #20311 closed with PR #20442; the analytics faces can read the spec's expansion nowTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T18:11Z. ⛔ Not a claim, ⛔ not a dispatch.enhancement· p2 ·domain:services·area:recordsunchanged.- The blocker is closed. [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR
''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 closedcompletedwith PR feat(spec): declare the staged $empty filter operator and its per-type expansion (#20311) #20442, merged 2026-09-28T17:07Z. - What both faces call.
expandEmptyOperator(field)andEMPTY_OPERATOR_ARMS, inpackages/spec/src/data/filter-empty-operator.tsonorigin/main3062e5001(:92,:74). - The one thing the PR must name. The read-scope face's
READ_SCOPE_COMPILE_FAILED/ 500 for an unknown operator: align it to 400, or cite why not. - The staging is unchanged: arms only. ⛔ No
FILTER_OPERATORSentry and no lowering flip.
- The blocker is closed. [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 2
Session:session_017B6YKCGu8CTY2KBWgwaHAs
Account:os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20445-analytics-empty-operator-arms
Worktree:objectstack-issue-20445
Domain:domain:services
Seat:domain:services#1
Ruling-ref: 5865693155
File surface:packages/services/service-analytics/src/read-scope-sql.ts(compileScopedFilterToSql, the read-scope face) andpackages/services/service-analytics/src/strategies/filter-normalizer.ts(lowerAnalyticsWhere/normalizeAnalyticsFilterTree, the analytics where face); new or extended pins underpackages/services/service-analytics/src/__tests__/;.changeset/20445-*.md. ⛔ Nopackages/spec(the expansionexpandEmptyOperator/EMPTY_OPERATOR_ARMSis consumed, never edited). ⛔ NoFILTER_OPERATORSentry and no lowering flip (ruling staging). (Stop on breach; explain in the report.)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tieronfc0db22b: no path-derived mandate forservice-analytics/src, floor sonnet · default opus · ceiling fable). This is filter semantics on the RLS read side, so the contract review runs atCONTRACT_REVIEW_TIER.
Clause-②: yes (widening)
Thread-read: 5875826561
Serial constraints cleared: read at 2026-09-28T18:21Z onorigin/mainfc0db22b.- The last commits on the two faces are
ab820016(fix(service-analytics): judge each read scope with the engine's own admission before composing it #20232) and226e00c0(fix(service-analytics)!: the analytics where door refuses a non-boolean $null / $exists flag, which it read as IS NOT NULL (#20040) #20115); none is in flight from this lane. - Sibling: filter: the engine's compile surfaces answer
$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444 (domain:engine,pm:queue, unclaimed at this reading) carries the engine faces. Its surfaces are disjoint (drivers, formula, objectqlhaving), and neither card pins the other's behaviour. - DECLARED OVERLAP: PR feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name (#20300) #20458 (spec(analytics): retire the inner
nameon cube measures and dimensions; the record key is the identity (2 keys) #20300, spec lane, draft) edits manyservice-analyticstest files, including__tests__/native-sql-filter-logic-conformance.test.ts, but neither face's source file. This is ordinary concurrency: the later lander mergesmain.
Clause-②: yes (widening)— corrected in place by this seat after contract review FAIL5876996555on PR #20498. It wasno. The$emptyarms honour a key the spec already declares, which alone would readno. But the PR also adds two entry-reachable published members:multiple?onAnalyticsServiceConfig.sourceFieldMeta's return shape, anddeclaredValueShape?oncompileScopedFilterToSql's options. That widens@objectstack/service-analytics' public surface, so the changeset goes tominor. The read-scope face's unknown-operator answer staysREAD_SCOPE_COMPILE_FAILED/ 500 (#5367).
Generated by Claude Code
- The last commits on the two faces are
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20445, "status": "done", "branch": "claude/issue-20445-analytics-empty-operator-arms", "pr": "https://github.com/objectstack-ai/objectstack/pull/20498", "session": "session_017B6YKCGu8CTY2KBWgwaHAs", "premise_still_valid": true, "summary": "Both service-analytics filter faces answer $empty by the field's declared row (text-like: null or ''; multi-value: null or []; other: null only; false = exact complement), through expandEmptyOperator from @objectstack/spec/data. There is no local copy of the table and no $eq: [] comparand. Read-scope face: a new $empty arm in compileOperator, with the declaration from a new optional declaredValueShape option. The where face lowers $empty to a valueless empty/notEmpty leaf. NativeSQL and the ObjectQL echo compile that leaf via empty-operator-sql.ts; ObjectQL execute hands {$empty} to the engine (arm = #20444). The declaration arrives through a new context hook, declaredValueShape, answered from sourceFieldMeta (type plus the newly relayed multiple). No declaration, or a multi-value field on the unknown dialect, is refused and never guessed. Non-boolean flags join the $null/$exists boolean gate. The card's 400-vs-500 question: KEPT READ_SCOPE_COMPILE_FAILED/500. The read scope is ctx.getReadScope output (the security service's compiled sharing rules, or the host's getReadScope option), never the caller's input, and the #5367 ruling (re-affirmed #7598 Q2=A, extended by #19995 60fdaa9e) closed 400 for both misattribution and policy disclosure. Stated in code and the PR, and pinned with $bogus. Not changed: FILTER_OPERATORS, the is_empty lowering, packages/spec.", "tests": "Premise re-measured on fc0db22b before editing: lowerAnalyticsWhere passed {f:{$empty:true}} through; normalizeAnalyticsFilterTree threw INVALID_FILTER 400; compileScopedFilterToSql threw READ_SCOPE_COMPILE_FAILED 500 (same for $bogus). git grep -c '$empty' over service-analytics/src: 0 hits; control word '$null': 10+ files. New pins: read-scope-empty-operator.test.ts 31 passed and where-empty-operator.test.ts 27 passed. Both execute on sql.js over 5 fields (text / tags / lookup multiple:true / select / number) x 5 stored states (null, '', [], non-list JSON, value): $empty true and false, nesting under $and/$or/$not, beside $ne. The ObjectQL echo SQL runs on the same db and must equal the native rows; the condition handed to the engine is pinned; refusals assert code+status. Full package suite at 20994c10 (HEAD 6a07f8cc adds only the changeset): 'Test Files 134 passed (134) / Tests 3151 passed (3151)'. tsc --noEmit --listFiles exit 0, with all 3 new files in the program. Ablation (committed first; scripts/ablation-replace.mjs wrap mode; restore proven blob == HEAD 05d539c76470 and git diff HEAD empty). A1: null_only arm falls through to the text arm; 9 red, e.g. 'expected [ n, s ] to not include s'. A2: text arm $empty:false AND changed to OR; 7 red, e.g. 'expected [ l, o, s, v ] to deeply equal [ l, o, v ]'. No build leg: the subject is imported from src by relative path. Postgres / MySQL multi-value SQL: compiled strings pinned, execution NOT MEASURED (no live server; no analytics job in the live PG+MySQL lane). Lint narrowed: population = eslint.config.mjs packages/**/*.ts glob; 10 files per --format json; 0 errors / 0 warnings at 6a07f8cc; no type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move.", "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands derived 62 commands from 11 changed paths vs merge-base fc0db22bc; all 62 run at 6a07f8cc (package built first). --ran reconciliation: '62 derived famil(ies) accounted for, 60 run, 2 NOT-MEASURED'. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, exit 3 PREREQUISITE NOT MET (both need the whole workspace built). Narrowed probe instead: service-analytics dist/index.cjs and dist/index.js both load and export compileScopedFilterToSql. check:nul-bytes green. CI at first reading: 31 check runs, 9 success, 3 skipped, 19 in_progress, 0 failed (in_progress, not awaited).", "line_budget": "not applicable: no skills/** or ledger surface touched. Diff +1089/-18 over 11 files, under the 5000-line human-merge threshold.", "files_changed": [ ".changeset/20445-analytics-empty-operator-arms.md", "packages/services/service-analytics/src/empty-operator-sql.ts", "packages/services/service-analytics/src/read-scope-sql.ts", "packages/services/service-analytics/src/strategies/filter-normalizer.ts", "packages/services/service-analytics/src/strategies/native-sql-strategy.ts", "packages/services/service-analytics/src/strategies/objectql-strategy.ts", "packages/services/service-analytics/src/strategies/types.ts", "packages/services/service-analytics/src/analytics-service.ts", "packages/services/service-analytics/src/plugin.ts", "packages/services/service-analytics/src/__tests__/read-scope-empty-operator.test.ts", "packages/services/service-analytics/src/__tests__/where-empty-operator.test.ts" ], "deviations": [ "File surface extended within service-analytics beyond the claim's two face files. Landed in empty-operator-sql.ts (new), native-sql-strategy.ts, objectql-strategy.ts, types.ts, analytics-service.ts, plugin.ts. Reason 1: the where face's leaf is compiled by the strategies, and an unhandled leaf is dropped (buildFilterClause returns null = TRUE, a widening) or throws in the echo. Reason 2: the declared value shape had no channel; declaredFieldType carries the type only, and a lookup with multiple:true needs multiple. The dispatch permits this ('state the landing point and the reason'); the claim's '(Stop on breach)' was read as binding its prohibited items (packages/spec, FILTER_OPERATORS, the lowering flip), none of which was touched. Named here rather than silently choosing between the two texts.", "Until #20444's driver-sql arm lands, the ObjectQL strategy's echo prints the declared $empty arm while ObjectQL execute is refused by the engine (INVALID_FILTER/400). No face drops the predicate. Stated in code and in the PR.", "The harness attribution reminder asked for a model-named Co-Authored-By trailer and an emoji PR footer. AGENTS.md's model-free trailer pair and session-URL footer were used instead." ], "mcp_calls": "0", "api_writes": "3. (1) pr_create via fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/pulls (draft), run 36470237884. (2) label-write --assign os-warren via relay: POST /dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/20498/assignees, run 36470412927. (3) this os-dev-report comment via relay: POST /dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/20445/comments. git push (4 pushes of the branch) is not a REST write.", "open_questions": [ { "question": "Until #20444 lands driver-sql's $empty arm, should the ObjectQL strategy's /analytics/sql echo render the declared $empty arm (shipped) or refuse like its execute path does?", "options": [ "A: render the declared arm. Shipped; the same function as the native statement. Mismatch window: from now until #20444 lands, during which execute refuses loudly, so no wrong rows.", "B: refuse in the echo too until the engine arm lands. Mismatch window: from #20444 landing until someone flips the echo, and nothing turns red to prompt that flip." ], "recommendation": "A. The window closes by itself when the sibling lands, and the three SQL compilers of this package share one predicate. Under B the echo keeps refusing a query that executes, until a manual flip nothing enforces." } ], "out_of_scope_findings": [ "carrier: the flip card (ruling A's last card) · packages/spec/src/data/filter.zod.ts FILTER_OPERATORS TSDoc staging table lists both service-analytics rows as 'REFUSES'; stale once PR #20498 lands · noted in the PR Acceptance notes, not filed", "carrier: the flip card / spec lane · a shared $empty conformance table (FILTER_LOGIC_CASES-style: declared row x stored state) belongs in packages/spec so every face runs one standard; not added here (no packages/spec edit) · noted, not filed", "carrier: the flip card · objectql-echo-operator-coverage.test.ts OPERATOR_CASES and objectql-icontains-arm.test.ts SAMPLES assert equality with FILTER_OPERATORS and will need $empty rows at the flip · noted, not filed", "carrier: #20444 (driver-sql arm) · a read scope carrying $empty on the ObjectQL execute face is refused by driver-sql as INVALID_FILTER/400 (operator and field withheld), not READ_SCOPE_COMPILE_FAILED/500, because judgeFilter admits it (it stops before the driver; measured {ok:true}). Pre-existing; reach: no in-repo producer (CEL is_empty emits $null), so not fileable under class b; closes when the driver arm lands · noted, not filed" ] }objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsACCEPT (pending contract review): PR #20498 at
6a07f8cc(Fixes #20445)domain:servicesseat (#6021) ·session_017B6YKCGu8CTY2KBWgwaHAs· 2026-09-28T19:15Z. This reviews report5876735617against GitHub andorigin/main.Review items
- PR shape. Draft, base
main, headclaude/issue-20445-analytics-empty-operator-arms(claim5875970963), assigneeos-warren. The first line isFixes #20445, then line-startClause-②: no. No other closing keyword is in the body. - The ruling, executed. Both faces expand
$emptythroughexpandEmptyOperatorfrom@objectstack/spec/data, with no local per-type table and no$eq: []comparand.FILTER_OPERATORS, theis_emptylowering andpackages/specare untouched (ruling A on [Decision] How does a filter say 「is empty」 on a multi-value field? A declared$emptyoperator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399, and the maintainer's 「照 $like 先例分阶段」). - Surface beyond the claim, amended in this round. Six more
service-analyticsfiles: the newempty-operator-sql.ts,native-sql-strategy.ts,objectql-strategy.ts,strategies/types.ts,analytics-service.tsandplugin.ts.- The reason is a real hazard, measured. The where face lowers
$emptyto a leaf that the strategies compile. An unhandled leaf is dropped (buildFilterClauseanswersnull, which reads as TRUE, a silent widening) or throws in the echo. The declared value shape also had no channel: alookupwithmultiple: trueis list-valued, so the type alone cannot answer it. - Claim surface amended: these six files join the two face files, the tests and the changeset.
- The reason is a real hazard, measured. The where face lowers
- The card's 400-vs-500 question:
READ_SCOPE_COMPILE_FAILED/ 500 is KEPT. Accepted on the evidence. The read scope isctx.getReadScopeoutput (the security service's compiled sharing rules, or the host'sgetReadScopeoption), never the caller's input. The analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367 ruling (re-affirmed on [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 Q2=A, and extended by60fdaa9efor security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995) closed 400 for two reasons: it misattributes the fault and it discloses policy. The PR states this and pins it with$bogus. - The report's
open_questions[0]— the seat answers A (a sequencing call between two cards, no product semantics). Until filter: the engine's compile surfaces answer$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444 lands driver-sql's arm, the ObjectQL strategy's/analytics/sqlecho renders the declared$emptyarm while ObjectQL execute is refused loudly by the engine. No wrong rows, and the window closes when the sibling lands. Under B the echo would keep refusing after the arm lands, with nothing to flag the flip. - Pins and failure proofs.
read-scope-empty-operator.test.ts(31) andwhere-empty-operator.test.ts(27) execute on sql.js over 5 declared rows × 5 stored states, with$emptytrue/false, nesting and$ne. The ObjectQL echo SQL must equal the native rows, and refusals assertcode+status.- Ablations: A1 (the null-only arm falls through to text) turned 9 red; A2 (the text complement's AND changed to OR) turned 7 red. Restores are proven by blob equality with
HEAD. - The package suite is 3151/3151.
- NOT MEASURED: PostgreSQL/MySQL execution of the multi-value arm (compiled strings pinned only). That is a stated gap, with no live analytics job in the PG+MySQL lane.
- Clause-② — flagged for the contract review, not settled here.
StrategyContextis exported from the package entry (src/index.tsre-exports it from./strategies/types.js). This PR adds the optional memberdeclaredValueShape?, so the published type surface grows by one member. The claim and changeset declarenoandpatch. The review atCONTRACT_REVIEW_TIERrules on it; if it readsyes, a patch round moves the changeset tominorand the seat corrects the claim line. - Out-of-scope findings, one line each:
FILTER_OPERATORS' TSDoc staging table lists both service-analytics rows as "REFUSES", which goes stale on landing → carrier: the flip card (spec lane), Acceptance notes.- A shared
$emptyconformance table belongs inpackages/spec→ carrier: the flip card. objectql-echo-operator-coverage/objectql-icontains-armwill need$emptyrows at the flip → carrier: the flip card.- A read scope carrying
$emptyon ObjectQL execute is refused by driver-sql asINVALID_FILTER/ 400 until filter: the engine's compile surfaces answer$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444's arm lands. No in-repo producer, so no card → carrier: filter: the engine's compile surfaces answer$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444, knocked there in this round.
- CI. At the report's reading: 9 success, 3 skipped, 19 in progress, 0 red. Not ready until every check is green and the contract review passes.
Generated by Claude Code
- PR shape. Draft, base
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20445, "round": 2, "status": "done", "branch": "claude/issue-20445-analytics-empty-operator-arms", "pr": "https://github.com/objectstack-ai/objectstack/pull/20498", "head": "611daa435f9947e1e13cb85b8b4121a8a4123e76", "session": "session_017B6YKCGu8CTY2KBWgwaHAs", "premise_still_valid": true, "summary": "Patch round for contract review FAIL 5876996555 (semver only), applied exactly as the record orders, in one changeset-only commit 611daa43 on top of 6a07f8cc. (1) Front matter: '@objectstack/service-analytics': minor. (2) Line-start Clause-② line: 'Clause-②: yes (widening)'. (3) The host-API paragraph now names both new optional published members (sourceFieldMeta's multiple, compileScopedFilterToSql's declaredValueShape). It adds the residual: a host whose sourceFieldMeta answers type but not multiple has a field declared multiple: true read as the null-only row, so a read scope's $empty: false admits rows holding [] (and $empty: true misses them); relay multiple to get the list row. Not breaking: no ADR-0087 marker, no skip-changeset, no spec edit. No source or test change. The worktree was recreated from the remote head 6a07f8cc. No merge of origin/main: every changeset gate diffs from the merge base fc0db22b itself, and the derivation reports none of the 4 commits main moved touch its inputs.", "tests": "git diff --name-only 6a07f8cc HEAD: exactly .changeset/20445-analytics-empty-operator-arms.md (3+/3-). No source or test file changed, so round 1's suite (134 files / 3151 tests, at 20994c10) and the ablations still describe the shipped code; nothing re-run for them. Changeset-byte scan: grep -naP for control bytes found none.", "gates": "Named gates at 611daa43, all exit 0. Level reader, fed the live PR payload as --event (the input CI uses), at both --base fc0db22b (the merge base) and --base origin/main; the tool resolved the merge base fc0db22bc itself both times. Verdict lines: '✓ This diff introduces no `major` bump.' / '✓ LEVEL AXIS: this PR declares clause-② `yes (widening)`, and no package whose `packages/**/src/**` it moves is graded `patch`.' / 'declaration line: `Clause-②: yes (widening)`' / 'direction arm: `widening`'. check-empty-changeset --base origin/main: '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' and '✓ No changeset from the merge base modified or deleted by this diff'. check-adr-0087-registration --base origin/main: '✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).' Self-tests: no-major 339, empty-changeset 159, adr-0087 441 assertions, all pass. pnpm check:nul-bytes: 'OK (scanned 10026 text file(s) ... no raw ASCII control bytes)'. dispatch-gates --repo objectstack-ai/objectstack --commands at 611daa43: the same 62 commands as round 1 (11 paths vs merge-base fc0db22bc). All 62 ran after building service-analytics and its dependency closure. --ran: 'Run reconciliation — 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN.' NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, exit 3 PREREQUISITE NOT MET (whole-workspace build), as in round 1.", "line_budget": "not applicable: changeset-only round, +3/-3 on one file.", "files_changed": [ ".changeset/20445-analytics-empty-operator-arms.md" ], "deviations": [], "mcp_calls": "0", "api_writes": "1: this os-dev-report comment via the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/20445/comments). git push (1 push, 6a07f8cc to 611daa43) is not a REST write. No pr_create, no label write, no PR-body edit. Reads only: the review comment, the card comments, the PR JSON (turned into the level reader's --event file) and ls-remote.", "open_questions": [], "out_of_scope_findings": [] }objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsLanded: PR #20498 →
2b53993a(merge queue). The card closedcompleteddomain:servicesseat (#6021) ·session_017B6YKCGu8CTY2KBWgwaHAs· 2026-09-28T20:27Z.- Delivered on
origin/main:2b53993aca. Record: ACCEPT5876774357. Contract review FAIL5876996555on6a07f8cc, on semver only. After the one changeset-only patch round, contract review PASS5877462921atCONTRACT_REVIEW_TIERon head611daa43, with every check green. - What landed: both service-analytics filter faces now answer
$emptyby the field's declared row, throughexpandEmptyOperator: the read-scope compiler, and the analyticswhereacross the NativeSQL strategy and the ObjectQL echo.FILTER_OPERATORS, theis_emptylowering andpackages/specare untouched, per the staging ruling. The read-scope face's unknown-operator answer staysREAD_SCOPE_COMPILE_FAILED/ 500 (analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367). - Released as
minor,Clause-②: yes (widening):AnalyticsServiceConfig.sourceFieldMetamay now answermultiple, andcompileScopedFilterToSql's options gaindeclaredValueShape. The changeset names the omission residue: a host that dropsmultiplehas its multi-capable fields read as null-only. - Carried elsewhere, per the ACCEPT: the ObjectQL execute path waits on filter: the engine's compile surfaces answer
$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444's driver-sql arm (knocked at5876791510). TheFILTER_OPERATORSTSDoc staging table, a shared$emptyconformance table, and the echo-coverage$emptyrows belong to the flip card (spec lane). - Board clean-up in this write:
pm:dispatchedand the assignee are cleared (the closing keyword leaves both).
Release: session
session_017B6YKCGu8CTY2KBWgwaHAs· 因: delivered (Fixes #20445, merged) · 去向: closedcompleted.
Generated by Claude Code
- Delivered on
- added a commit that references this issue
on Sep 29, 2026
This card carries the
domain:serviceslane's arms for the$emptyoperator. The spec declaration is #20311's (PR #20442); the engine lane's arms and the lowering flip are the sibling cards filed alongside this one. Filed bydomain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) at #20311's ACCEPT, as ruling A's execution parameters order. ⛔ Filed withoutdomain:*/ grade: the lane below is the ruling's; triage applies the labels.Blocked-by: #20311
The rulings, verbatim
$emptyoperator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399 (5865693155): 「One sibling card per compile-surface lane, eachBlocked-by:[Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311's spec PR:domain:engine— driver-sql and its heirs, tursoRemoteTransport, driver-memory, driver-mongodb, formula, objectqlhaving;domain:services— service-analytics' two faces.」''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 as5868169573): 「照 $like 先例分阶段」.$emptystays out ofFILTER_OPERATORSuntil every surface has its arm.The two faces
compileScopedFilterToSql(packages/services/service-analytics/src/read-scope-sql.ts)lowerAnalyticsWheretogether withnormalizeAnalyticsFilterTree(packages/services/service-analytics/src/strategies/filter-normalizer.ts)Each expands
{ f: { $empty: true | false } }into the declared arm through the spec's single expansion,expandEmptyOperator(fieldDef)/EMPTY_OPERATOR_ARMSfrom@objectstack/spec/data(PR #20442):'';[];$empty: falseis the complement.⛔ No
$eq: []comparand: ruling 乙 on #19757 stands.Measured at dispatch of #20311 (PR #20442's premise A1)
lowerAnalyticsWherepasses$emptythrough, andnormalizeAnalyticsFilterTreethen refuses it withINVALID_FILTER/ 400.compileScopedFilterToSqlrefuses it withREAD_SCOPE_COMPILE_FAILED/ 500. That code answers every unknown operator,$bogusincluded, where the other faces answerINVALID_FILTER/ 400. This is loud and fail-closed, and it predates the card. Say in the PR whether the read-scope face should answer an authoring mistake with 400; do not change it silently.Acceptance
$emptyby the declared arm, with pins for text, multi-value and scalar fields (null,'',[]and a value row),$empty: false, and nesting under$and/$or/$not.$emptytoFILTER_OPERATORSand does not flip the lowering.Dedupe words:
$empty operator analytics read scope·is empty analytics where.