You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
analytics: /analytics/dataset/query refuses retired $regex / $options in runtimeFilter with a generic "Unsupported filter operator", not the RETIRED prescription naming $icontains #28945
Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
立卡门类别: ① 产品缺陷 — 具名落点 (packages/services/service-analytics/src/strategies/filter-normalizer.ts) + 复现; release-fixed error text (立卡门例外「发版固化的错误文字」)
reach: 公开入口实测一次错误结果 — POST /api/v1/analytics/dataset/query with runtimeFilter {"name":{"$regex":"acme"}} answers a generic "Unsupported filter operator" (two boots at 7fa9873)
业务理由: a team upgrading dashboards whose stored widget filters still use $regex sees a generic operator list instead of the one-line fix ($icontains) the D3 retirement entry promises "everywhere". The data door already gives it. Not doing it: each upgrader has to find the replacement by hand, on the door that serves exactly those stored filters.
动手的读者: the domain:server seat (service-analytics); triage first touch confirms the lane, then os-dev.
查重: RETIRED_FILTER_OPERATORS → 18 hits, 0 open; analytics "Unsupported filter operator" → 13 hits, 0 open (#20020, #20098 closed are different gates). No duplicate.
QA-source: #28942 · api-backend.enforce-or-remove-authoring-gates · observed beside the clauses
What happens.POST /api/v1/analytics/dataset/query refuses the retired $regex / $options operators in selection.runtimeFilter with a generic "Unsupported filter operator" message instead of the RETIRED prescription the data door gives. The refusal is safe (400 INVALID_FILTER, nothing widened); only the prescription is missing. This is the door that serves stored dashboard widget filters, which the D3 entry filter-regex-options-retired lists as a stored site and promises "refused everywhere with INVALID_FILTER / 400 naming the replacement".
Mechanism.packages/services/service-analytics/src/strategies/filter-normalizer.ts throws [analytics] Unsupported filter operator "$regex" on "name". Supported: $eq, … $icontains, … because MONGO_TO_CUBE_OP has no entry for it; it never consults RETIRED_FILTER_OPERATORS. The data door answers the same operator with "… is RETIRED … Write "$icontains" instead".
Reproduction (stock showcase boot at 7fa9873, admin; reproduced on two boots):
Read fresh. packages/services/service-analytics/src/strategies/filter-normalizer.ts refuses $regex / $options with the correct 400 INVALID_FILTER but a generic message, because it never reads RETIRED_FILTER_OPERATORS. Nothing is widened and only the error wording is wrong, so it is p3 under the copy rule in domain:server. The location is named, so it goes to pm:queue, and under 「低优先级的先不考虑」 it waits behind p1/p2 work.
Claim: PM loop round 4 · card thread · 2026-11-02T05:22Z
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5oAC7wmJvoM1VnnCjg
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-jebtpj (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. With 2026-11-01T12:11Z 「定级你可以自己决定」 (triage graded this card) and 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」; seat 1 split the queue and gave this card to seat 2. Seat's route: a retired filter operator in an analytics runtimeFilter gets the same 400 INVALID_FILTER with the data door's RETIRED prescription, read from RETIRED_FILTER_OPERATORS and the same message builder.
File surface: packages/services/service-analytics/src/strategies/filter-normalizer.ts and every analytics face that runs it; their tests; one dogfood pin; changeset
Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: medium
Clause-②: no expected (one refusal's words replaced by another's, precedent #28615); the dev measures and declares
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the dev reuses the retirement table and message builder in the analytics normalizer, red first with ablation; the seat reviews and lands
Thread-read: none
Premise: measured by triage (6569498837) on main
Serial constraints cleared: none; overlap re-read of open PRs on service-analytics/src/strategies/ before pr_create
Resume-from: none — fresh from origin/main
State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.
domain:server#2 · os-project-manager · session_01FEkQ5oAC7wmJvoM1VnnCjg
os-dev-report
{
"issue": 28945,
"status": "done",
"branch": "claude/project-thread-jebtpj",
"pr": "#29017",
"session": "session_01FEkQ5oAC7wmJvoM1VnnCjg",
"premise_still_valid": true,
"summary": "A retired filter operator ($regex, $options) in an analytics where / dataset runtimeFilter now answers 400 INVALID_FILTER with the data door's sentence: RETIRED, Write "$icontains" instead., then RETIRED_FILTER_OPERATORS[op].why verbatim, naming every retired sibling of the same constraint. One pass (assertNoRetiredOperators) is the last step of normalizeWhereComparands, so the native execute, /analytics/sql echo, ObjectQL engine path, AnalyticsService.query/generateSql, queryDataset (runtime, scope and measure filters) and the draft preview give the same message from one call; unknown operators keep the generic list and no existing refusal moves. Declared conflict with the order: no shared message builder exists on main (each of the five data-door faces composes its own lead-in around the table's to/why), so the lead-in follows driver-sql's words from the same table. Patch changeset; Clause-②: no, Metadata-protocol: none; one dogfood pin on the showcase dataset door.",
"tests": "HEAD 49e31fc, all under os-verify-lock VERDICT command-exit 0: service-analytics test Test Files 205 passed (205), Tests 4907 passed | 282 skipped (5189); service-analytics typecheck exit 0; new retired-filter-operator-refusal.test.ts Tests 18 passed (18); dogfood analytics-retired-filter-operator.dogfood.test.ts Tests 5 passed (5); dogfood typecheck exit 0 (--listFilesOnly includes both new files). Red first (base blob 135054e over filter-normalizer.ts, trap restore, blob==HEAD after): unit Tests 11 failed | 7 passed (live faces answered the generic Unsupported filter operator list, the draft preview its own not-evaluated sentence); live showcase dogfood with service-analytics rebuilt from the base blob (ablation-dist-preflight --absent: marker gone from 6 built files) Tests 3 failed | 2 passed, $regex/$options got 400 [analytics] Unsupported filter operator ... (the card's answer). Ablation (ablation-replace --delete of the assertNoRetiredOperators call, anchor 1->0, blob 34fb0eb6->e59a5023, src import, no dist): Tests 12 failed | 6 passed (18), the 12 are every RETIRED case, the 6 are the controls; restore blob 34fb0eb6 == HEAD, git diff HEAD empty. One existing pin (filter-normalizer-not-null-safe.test.ts, #5325) used $regex as its unknown-operator example; it now uses $sounds_like and pins $regex beside it as the RETIRED refusal.",
"gates": "Local: dispatch-gates --commands at 49e31fc derived 64 commands; all 64 ran, each exit 0 (check-plugin-teardown-shape --self-test exited 3 on the shallow clone first, 0 after git fetch --depth=1 origin 621a487...); dispatch-gates --ran: 64 derived accounted for, 64 run, 0 NOT-MEASURED. Record: pm-orders/server/28945-gates-ran.txt and the PR body. CI families (roster, wide-population, type-check lanes, lint, Test Core): in_progress on PR #29017 at report time, not re-run locally.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the new dogfood file ran locally, 5/5",
"mcp_calls": "0",
"api_writes": "2 — POST repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create, PR #29017, read-back identical, no assignees); POST repos/objectstack-ai/objectstack/dispatches (this os-dev-report comment via post-stamped). git push of the harness branch is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"observation, not filed (no wrong answer): the analytics door serves a refusal message whole while the data door cuts it at 500 characters with an ellipsis; measured on the dogfood showcase, the $regex refusal is 1068 characters on /analytics/dataset/query and 500 on /data/showcase_project/query · carrier: none · noted in PR Acceptance notes",
"possible follow-up, not filed: one spec-side builder for the retired-operator lead-in so the six faces (driver-sql, driver-memory, driver-mongodb, driver-turso remote transport, objectql having, service-analytics) stop composing it separately; none is wrong today · carrier: none · dedupe words: RETIRED_FILTER_OPERATORS builder, retiredFilterOperatorError, retired operator lead-in"
]
}
Landed: a retired filter operator ($regex, $options) in an analytics where or dataset runtimeFilter is refused with the retirement's prescription naming $icontains, read verbatim from RETIRED_FILTER_OPERATORS. The refusal is the last pass of normalizeWhereComparands, so /analytics/query, /analytics/dataset/query, /analytics/sql and the draft preview answer it the same way.
Unchanged: status and code (INVALID_FILTER / 400), the refused set, an unknown operator's generic list, and every other refusal this door gives.
Premise correction: the order's "the same message builder" does not exist on main; each data-door face composes its own lead-in. The PR follows driver-sql's lead-in word for word.
Follow-up: none.
Release: ships patch for @objectstack/service-analytics at the next release.
State: pm:dispatched and the assignee removed by this record. The card closes through Fixes #28945.
Resume-from: none.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: api-backend.enforce-or-remove-authoring-gates
Triage: priority:p3 · domain:server · pm:queue · 2026-11-02T05:03Z
⛔ Not a claim.
$regex/$optionswith the correct 400 INVALID_FILTER but a generic message, because it never readsRETIRED_FILTER_OPERATORS. Nothing is widened and only the error wording is wrong, so it is p3 under the copy rule indomain:server. The location is named, so it goes topm:queue, and under 「低优先级的先不考虑」 it waits behind p1/p2 work.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Claim: PM loop round 4 · card thread · 2026-11-02T05:22Z
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5oAC7wmJvoM1VnnCjgAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-jebtpj(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. With 2026-11-01T12:11Z 「定级你可以自己决定」 (triage graded this card) and 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」; seat 1 split the queue and gave this card to seat 2. Seat's route: a retired filter operator in an analytics
runtimeFiltergets the same 400INVALID_FILTERwith the data door's RETIRED prescription, read fromRETIRED_FILTER_OPERATORSand the same message builder.File surface:
packages/services/service-analytics/src/strategies/filter-normalizer.tsand every analytics face that runs it; their tests; one dogfood pin; changesetContainer & model:
M,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: mediumClause-②: no expected (one refusal's words replaced by another's, precedent #28615); the dev measures and declares
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the dev reuses the retirement table and message builder in the analytics normalizer, red first with ablation; the seat reviews and lands
Thread-read: none
Premise: measured by triage (
6569498837) on mainSerial constraints cleared: none; overlap re-read of open PRs on service-analytics/src/strategies/ before pr_create
Resume-from: none — fresh from
origin/mainState: →
pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-project-manager · session_01FEkQ5oAC7wmJvoM1VnnCjg
os-dev-report
{
"issue": 28945,
"status": "done",
"branch": "claude/project-thread-jebtpj",
"pr": "#29017",
"session": "session_01FEkQ5oAC7wmJvoM1VnnCjg",
"premise_still_valid": true,
"summary": "A retired filter operator ($regex, $options) in an analytics where / dataset runtimeFilter now answers 400 INVALID_FILTER with the data door's sentence: RETIRED, Write "$icontains" instead., then RETIRED_FILTER_OPERATORS[op].why verbatim, naming every retired sibling of the same constraint. One pass (assertNoRetiredOperators) is the last step of normalizeWhereComparands, so the native execute, /analytics/sql echo, ObjectQL engine path, AnalyticsService.query/generateSql, queryDataset (runtime, scope and measure filters) and the draft preview give the same message from one call; unknown operators keep the generic list and no existing refusal moves. Declared conflict with the order: no shared message builder exists on main (each of the five data-door faces composes its own lead-in around the table's to/why), so the lead-in follows driver-sql's words from the same table. Patch changeset; Clause-②: no, Metadata-protocol: none; one dogfood pin on the showcase dataset door.",
"tests": "HEAD 49e31fc, all under os-verify-lock VERDICT command-exit 0: service-analytics test Test Files 205 passed (205), Tests 4907 passed | 282 skipped (5189); service-analytics typecheck exit 0; new retired-filter-operator-refusal.test.ts Tests 18 passed (18); dogfood analytics-retired-filter-operator.dogfood.test.ts Tests 5 passed (5); dogfood typecheck exit 0 (--listFilesOnly includes both new files). Red first (base blob 135054e over filter-normalizer.ts, trap restore, blob==HEAD after): unit Tests 11 failed | 7 passed (live faces answered the generic Unsupported filter operator list, the draft preview its own not-evaluated sentence); live showcase dogfood with service-analytics rebuilt from the base blob (ablation-dist-preflight --absent: marker gone from 6 built files) Tests 3 failed | 2 passed, $regex/$options got 400 [analytics] Unsupported filter operator ... (the card's answer). Ablation (ablation-replace --delete of the assertNoRetiredOperators call, anchor 1->0, blob 34fb0eb6->e59a5023, src import, no dist): Tests 12 failed | 6 passed (18), the 12 are every RETIRED case, the 6 are the controls; restore blob 34fb0eb6 == HEAD, git diff HEAD empty. One existing pin (filter-normalizer-not-null-safe.test.ts, #5325) used $regex as its unknown-operator example; it now uses $sounds_like and pins $regex beside it as the RETIRED refusal.",
"gates": "Local: dispatch-gates --commands at 49e31fc derived 64 commands; all 64 ran, each exit 0 (check-plugin-teardown-shape --self-test exited 3 on the shallow clone first, 0 after git fetch --depth=1 origin 621a487...); dispatch-gates --ran: 64 derived accounted for, 64 run, 0 NOT-MEASURED. Record: pm-orders/server/28945-gates-ran.txt and the PR body. CI families (roster, wide-population, type-check lanes, lint, Test Core): in_progress on PR #29017 at report time, not re-run locally.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the new dogfood file ran locally, 5/5",
"mcp_calls": "0",
"api_writes": "2 — POST repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create, PR #29017, read-back identical, no assignees); POST repos/objectstack-ai/objectstack/dispatches (this os-dev-report comment via post-stamped). git push of the harness branch is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"observation, not filed (no wrong answer): the analytics door serves a refusal message whole while the data door cuts it at 500 characters with an ellipsis; measured on the dogfood showcase, the $regex refusal is 1068 characters on /analytics/dataset/query and 500 on /data/showcase_project/query · carrier: none · noted in PR Acceptance notes",
"possible follow-up, not filed: one spec-side builder for the retired-operator lead-in so the six faces (driver-sql, driver-memory, driver-mongodb, driver-turso remote transport, objectql having, service-analytics) stop composing it separately; none is wrong today · carrier: none · dedupe words: RETIRED_FILTER_OPERATORS builder, retiredFilterOperatorError, retired operator lead-in"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Landing record · PR #29017
domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-11-02T07:12Zc470581a(+547 / -1)git merge-base --is-ancestor c470581a origin/maingit ls-remote origin refs/heads/gh-readonly-queue/*matching 29017git grep -c assertNoRetiredOperators origin/main -- packages/services/service-analytics/src/strategies/filter-normalizer.ts$regex,$options) in an analyticswhereor datasetruntimeFilteris refused with the retirement's prescription naming$icontains, read verbatim fromRETIRED_FILTER_OPERATORS. The refusal is the last pass ofnormalizeWhereComparands, so/analytics/query,/analytics/dataset/query,/analytics/sqland the draft preview answer it the same way.INVALID_FILTER/ 400), the refused set, an unknown operator's generic list, and every other refusal this door gives.driver-sql's lead-in word for word.Release: ships
patchfor@objectstack/service-analyticsat the next release.State:
pm:dispatchedand the assignee removed by this record. The card closes throughFixes #28945.Resume-from: none.
Generated by Claude Code