Repository navigation
fix(service-analytics): a retired filter operator ($regex, $options) in an analytics filter is refused with the retirement's prescription naming $icontains - #29017
Conversation
…irement prescription A retired operator ($regex, $options) in an analytics where or dataset runtimeFilter was refused INVALID_FILTER / 400 with the generic unsupported-operator list. It now answers 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 at the end of normalizeWhereComparands, so the native execute, the /analytics/sql echo, the ObjectQL engine path and the draft preview all give the same answer. Unknown operators keep the generic list. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oAC7wmJvoM1VnnCjg
… the showcase dataset door The dogfood file asks the stock showcase's /analytics/dataset/query with $regex and $options in runtimeFilter and checks the RETIRED prescription, the same tail the data door prints, and two controls. The NULL-safe $not pin that used $regex as its example of an unknown operator now uses a truly unknown name, and pins the retired one beside it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oAC7wmJvoM1VnnCjg
…or does Plain table lookup over the constraint's $-keys (the package's lib has no Object.hasOwn), and drop an unused traversal parameter. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oAC7wmJvoM1VnnCjg
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 727bb7ee82737a61618ae5227b085863fb096b2b && git checkout 727bb7ee82737a61618ae5227b085863fb096b2b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab085a048241b0bf1fde06696fc6cbf659a6707f 49e31fc2d7bef8db79a449fa3e29cb0b3bf2f688 && git checkout -B drift-repro ab085a048241b0bf1fde06696fc6cbf659a6707f && git merge --no-ff 49e31fc2d7bef8db79a449fa3e29cb0b3bf2f688
node scripts/docs-audit/affected-docs.mjs --json ab085a048241b0bf1fde06696fc6cbf659a6707f |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #28945
Clause-②: no
Metadata-protocol: none
维护者速读
/analytics/dataset/query的runtimeFilter、/analytics/query、/analytics/sql、草稿预览)遇到已退役的$regex/$options时,仍答 400INVALID_FILTER,但文案改成和数据接口一样:说明该操作符已退役、写Write "$icontains" instead.,再逐字附上退役表里的处方。$regex过滤,原来只看到一长串"支持的操作符",找不到一行就能改好的写法;数据接口早已给出$icontains,分析接口应当说同一句话。patch。新检查放在过滤器归一化的最后一步,已有的拒收都先于它答复,未知操作符仍是原来的通用列表。回滚即还原本 PR。packages/services/service-analytics/src/strategies/filter-normalizer.ts(retiredFilterOperatorError、assertNoRetiredOperators、normalizeWhereComparands末尾的调用);src/__tests__/retired-filter-operator-refusal.test.ts;packages/qa/dogfood/test/analytics-retired-filter-operator.dogfood.test.ts。pnpm --filter @objectstack/service-analytics exec vitest run src/__tests__/retired-filter-operator-refusal.test.ts,预期 18 个全绿,其中各个分析入口对$regex给出同一句 RETIRED 文案。What changes
fieldLeavesinfilter-normalizer.tsrefused$regex/$optionswith the right envelope (INVALID_FILTER/ 400) but with the genericUnsupported filter operator "$regex" on "name". Supported: …list, because it never readRETIRED_FILTER_OPERATORS. The draft preview answered with its own… is not evaluated by the draft-data previewsentence instead. So the faces disagreed, and none of them named$icontains.retiredFilterOperatorError(op, field, siblings)builds the refusal the way each data-door face builds it:[analytics] Filter operator "OP" on field "FIELD" is RETIRED and is no longer evaluated., thenWrite "TO" instead., thenRETIRED_FILTER_OPERATORS[op].whyword for word, then every other retired key of the same constraint ({ $regex, $options }is one mistake with one fix). The lead-in isdriver-sql's (the data door on a SQL stack) without its "by this driver". It returnsnullfor a non-retired operator.assertNoRetiredOperators(node)walks the condition with the sharedforEachWhereFieldEntrytraversal, the same positions the shape, type and flag passes judge:$and/$ormembers,$not, field entries, and nested relation entries.normalizeWhereComparands, whichlowerAnalyticsWhereruns for the native execute, the/analytics/sqlecho and the ObjectQL engine path. The draft preview calls it itself, before its own operator vocabulary. Dataset scope filters and measure filters go through the same door.whereis still answered first. The only answer that changes is the one that used to fall to the unsupported-operator list. An unknown operator such as$sounds_likekeeps the generic list.CLIENT_MESSAGE_MAX), and the table'swhyputs everything an author must write in its opening sentences for that budget (data: a 400 INVALID_FILTER prescription is cut at 500 characters, before the alternative it names, so a caller using a retired filter operator is not told what to write instead #27098). A test pinsWrite "$icontains" instead.,`$contains` for a case-sensitive oneandmatch in application codeinside 500 characters.On the order's "the same message builder". No shared builder exists on main. The data door's five faces (
driver-sql,driver-memory,driver-mongodb,driver-turso's remote transport,objectql'shaving) each compose the lead-in themselves around the sharedto/why, and none of those builders is exported from a packageservice-analyticsdepends on (a driver dependency is the wrong direction). So this follows the same form from the same table, and no word of the prescription is written in this package. Folding all six into one spec-side builder would be a separate change (spec export, five drivers). It is noted below, not done here.Tests
Measured at HEAD
49e31fc2(worktree onclaude/project-thread-jebtpj, base135054eb).src/__tests__/retired-filter-operator-refusal.test.ts(new, 18 tests):wheredoor:$regexand a dangling$optionseach givecode+status+ the lead-in +Write "$icontains" instead.+ the table'swhyverbatim.{ $regex, $options }names the other key. The refusal also fires under$not, in an$orbeside a TRUE arm, in an$andand on a dotted member, and the client-cap budget holds. Precedence: a type-face defect in the samewherestill answers first./analytics/sqlecho, ObjectQL engine path,AnalyticsService.querynative and ObjectQL,AnalyticsService.generateSql, draft preview): each gives the message the door gives, byte for byte, with zero statements and zero engine calls.engine.findwith the samewhere(marked'author', as the SecurityPlugin marks a caller'swhere) refuses with the same tail fromWrite "$icontains" instead.on.queryDataset:runtimeFilter,runtimeFilterwithpreviewDrafts, the dataset scope filter and a measure filter all give the prescription, and no statement runs.$icontainsis served (['a1','a2']) on every live face.$sounds_likekeeps the generic list, and the draft preview keeps its own sentence for it. TheFilterArrayspelling['name','regex','acme']is still refused atisFilterAST.src/__tests__/filter-normalizer-not-null-safe.test.ts: the service-analytics 的第二个 SQL 编译器filter-normalizer.buildNode仍带着 #5297 的三条分叉:$not非 NULL-safe、{$not:{}}不加 WHERE、$or的{}析取项被丢 #5325 pin "an unknown operator inside a$notstill THROWS" used$regexas its example of an unknown operator. It now uses$sounds_likefor the generic list and pins$regexbeside it as the RETIRED refusal. The pin's intent (fail-closed under$not) is unchanged.packages/qa/dogfood/test/analytics-retired-filter-operator.dogfood.test.ts(new, booted stock showcase):POST /api/v1/analytics/dataset/query{ datasetName: "showcase_project_metrics", selection: { measures: ["project_count"], runtimeFilter: { name: { $regex: "acme" } } } }, and the same with$options, answer 400INVALID_FILTERwith the RETIRED lead-in,Write "$icontains" instead.and the table's opening sentence. The data doorPOST /api/v1/data/showcase_project/querywith the samewhereshows the same tail. Controls:$icontains→ 200,$sounds_like→ the generic list.Red first on main: unit suite and the live showcase door
The base blob of
filter-normalizer.ts(135054eb) was written over the file, under a trap restore.Tests 11 failed | 7 passed (18). The 11 failures are every RETIRED case. The live faces answered[analytics] Unsupported filter operator "$regex" on "name". Supported: $eq, $ne, …, and the draft preview answered[analytics] Filter operator "$regex" on field "name" is not evaluated by the draft-data preview. …. The 7 that passed are the controls plus the 500-character case, which passed vacuously on a message that held none of the texts. It was then tightened to require each text to be present (so the ablation below reds it).service-analyticsrebuilt from the base blob,ablation-dist-preflight … --absentconfirmed the marker gone from all 6 built files):Tests 3 failed | 2 passed (5).$regexand$optionsgot400 INVALID_FILTER[analytics] Unsupported filter operator …, which is the card's reported answer. The parity case failed, and the two controls passed.Restore: blob hash equals HEAD's and
git diff HEADis empty. Then a rebuild, and the preflight showed the marker present in 2 built files.Ablation: dropping the retired-table pass reds only the new cases
node scripts/ablation-replace.mjs --file …/filter-normalizer.ts --anchor ' assertNoRetiredOperators(node, path);\n' --delete -- …: anchor 1 → 0, blob34fb0eb6→e59a5023,calls-left: 0. The unit file imports the module fromsrc/(no dist involved).Tests 12 failed | 6 passed (18). The 12 failures are every RETIRED case. The 6 that passed are the controls ($sounds_likegeneric list, theFilterArrayrefusal, the$icontainsleaf and rows, the preview's own sentence for an unknown operator, and type-face precedence).34fb0eb6== HEAD,git diff HEADempty.Package suite, typecheck, dogfood file
All at HEAD
49e31fc2, throughscripts/pm/os-verify-lock.sh(VERDICT command-exit 0):pnpm --filter @objectstack/service-analytics build, thenablation-dist-preflight.mjs @objectstack/service-analytics 'is RETIRED and is no longer evaluated.': marker present in 2 built filespnpm --filter @objectstack/service-analytics test:Test Files 205 passed (205),Tests 4907 passed | 282 skipped (5189)pnpm --filter @objectstack/service-analytics typecheck: exit 0 (--listFilesOnlyincludes both test files)pnpm --filter @objectstack/dogfood exec vitest run test/analytics-retired-filter-operator.dogfood.test.ts:Tests 5 passed (5)pnpm --filter @objectstack/dogfood typecheck: exit 0 (--listFilesOnlyincludes the new dogfood file)!in the title and no(narrowing)arm). The dogfood job does not run on PRs; the new file was run locally above.Acceptance notes
6569498837). It was measured on main through the unit faces and the live showcase door.…. Measured on the dogfood showcase: the$regexrefusal is 1,068 characters on/analytics/dataset/queryand 500 on/data/showcase_project/query. Nothing is wrong in what either door says. Whether analytics should applyCLIENT_MESSAGE_MAXis not this card's question.read-scope-sql.ts). It is policy-authored, so its refusal staysREAD_SCOPE_COMPILE_FAILED/ 500 with the message withheld (analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367), and a retired operator there keeps its fail-closed sentence. A top-level{ $regex: … }(not a field constraint) keeps "Unsupported top-level filter operator", as the data door's faces have no retired arm there either. NOT MEASURED: what the data door says for aFilterArray['name', 'regex', 'acme']. This door refuses it atisFilterAST, unchanged.service-analytics/src/strategies/objectql-strategy.ts(the draft preview's date bucket). No hunk meets this PR's files. No other open PR touchesfilter-normalizer.tsor the two service-analytics test files. All 27 open PRs were checked by file list.wherewhose only defect is a retired key now gets the RETIRED sentence instead of the generic list. The draft preview now gives the published faces' sentence instead of its own for those two keys.Verification
Derived gate families:
node scripts/pm/dispatch-gates.mjs --commandsat49e31fc2(5 paths vs merge base135054eb) gives 64 commands. All 64 ran locally and exited 0.dispatch-gates --ran:64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED. The first run ofcheck-plugin-teardown-shape.mjs --self-testexited 3 on the shallow clone. Aftergit fetch --depth=1 origin 621a4876…it exited 0 (48 cases). CI owns the 50 artifact-roster families, the wide-population families, the type-check lanes andpnpm lint; their verdicts are on this PR's check-runs.Gate record (COMMAND :: exit N), HEAD 49e31fc
Generated by Claude Code