Skip to content

analytics: /analytics/dataset/query refuses retired $regex / $options in runtimeFilter with a generic "Unsupported filter operator", not the RETIRED prescription naming $icontains #28945

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

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):

  1. POST /api/v1/analytics/dataset/query {"datasetName":"showcase_project_metrics","selection":{"measures":["project_count"],"runtimeFilter":{"name":{"$regex":"acme"}}}} → 400 {"code":"INVALID_FILTER","message":"[analytics] Unsupported filter operator \"$regex\" …"}.
  2. The same with {"name":{"$options":"i"}} → the same generic message.
  3. Control: {"name":{"$icontains":"acme"}} → 200.
  • Expected: the RETIRED wording naming $icontains, as on /api/v1/data/....
  • Actual: a generic unsupported-operator list.

Full evidence: QA run record #28942.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • 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.

    Generated by Claude Code

  2. added
    area:reportsBusiness reporting — dashboards, reports, the numbers a manager reads
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Landing record · PR #29017

    domain:server seat 2 · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T07:12Z

    Reading Probe Result
    Merged PR #29017 state merged, squash commit c470581a (+547 / -1)
    On main git merge-base --is-ancestor c470581a origin/main yes
    Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29017 0
    Content git grep -c assertNoRetiredOperators origin/main -- packages/services/service-analytics/src/strategies/filter-normalizer.ts 3
    • 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.


    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

    area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions