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
Commit 79d5dac
Browse filesBrowse the repository at this point in the historyBrowse files
fix(objectql): temporal-comparand and text-operator filter refusals state the prescription inside the client message cap (#27336)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKURKuRGF1ewEVVZKx7U1i4)_Fixes#27173
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:查询过滤里写了日期字段读不懂的值(如 `"last_30_days"`、年份超范围、不存在的日子、非 ISO
写法、解析后超范围的相对日期占位符),或对数字/布尔等字段用了文本操作符(`$contains` 等)时,`400
INVALID_FILTER` 报错现在先说"错在哪",紧接着说"该怎么写",最后才讲"没执行"和后果。只动了 objectql
里这两处报错的句子顺序和措辞。
- **为什么改**:客户端只读得到报错前 500 个字符。原文案把"该怎么写"放在最后,测得 306 个样例里 172
个连补救句的开头都被截掉,281 个补救不完整;开发者(或 AI)只知道错了,不知道改成什么。
- **风险与回滚**:低。500
字符上限、截断方式、错误码与状态码都没变,被拒绝的过滤集合也没变;只是同一条报错里句子换了顺序,`datetime`
的补救句改为先给相对日期占位符、年份范围补救句缩短。回滚即 revert。
- **先看**:`packages/objectql/src/temporal-comparand-door.ts`(六处 throw 与
`REMEDY` /
`YEAR_CLASS`);`packages/objectql/src/text-operator-declared-type-door.ts`(一处
throw);`packages/objectql/src/filter-refusal-prescription-cap.test.ts`(新
pin)。
- **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run
--maxWorkers=2 src/filter-refusal-prescription-cap.test.ts`,预期 31/31
通过;把这两个 producer 换回 `main` 版本再跑,31 条全部失败。
- **需要你做的**:无,席位落地。
## What changes
Two `INVALID_FILTER` / 400 producers on `where`,
`aggregations[i].filter` and `having` printed their prescription last,
after the consequence and "NOT applied". A client reads at most 500
characters (`CLIENT_MESSAGE_MAX` in `@objectstack/types`, cut with an
ellipsis), so the prescription was often cut. Each refusal is now
ordered: the verdict (what was written, where, and why it is not a value
of its kind), then the prescription, then "NOT applied" and the
consequence. The cap, the truncation, the code, the status and the set
of refused filters are unchanged, as the triage ruled (6465970858): "⛔
The same rule as #27098: the prescription goes first, at the producer,
and the cap and envelope stay unchanged."
- **`temporal-comparand-door.ts`**: all six throw sites (junk, year
class, misread class on `where` and `having`; the resolved-placeholder
refusal on both) move the remedy ahead of "NOT applied". The verdict
clause shortens to "so it is not a KIND value" on the year and misread
arms (the junk arm keeps "which is not a KIND value this platform can
interpret"), and the placeholder arms drop the repeated "the years a
KIND value may name". `REMEDY.datetime` now names the relative-date
placeholder first, as its own doc comment always said it did, and is
shorter; the two year-class remedies are shorter. A module section,
"Prescription first, inside the client cap", records the rule.
- **`text-operator-declared-type-door.ts`**: "Compare FIELD with an
operator its declared type can answer …" now follows "A TYPE value is
never a string on any backend." The no-match-rule explanation moves
after "NOT applied".
- **`engine-aggregate-having-temporal-door.test.ts`**: the case that
read the remedy after "NOT applied" now reads it between the verdict and
"NOT applied", on both `having` and its `where` twin, and still requires
them equal.
### Measured: remedy position in the message (306 messages, matrix
below)
Matrix: three name sizes (object/field 1/1, 12/12, 25/31 characters) ×
every refusal class × `where` and `aggregations[0].filter` (flat and
inside a nested `$and` / `$or`) and `having`. "Start past cap" = the
remedy's first character at or past 499; "remedy cut" = the remedy is
not whole in what the client reads. Before = `origin/main` `4d3a077f`,
after = this head.
| Producer arm | n | length before → after | remedy start before → after
| start past cap | remedy cut |
|:--|--:|:--|:--|:--|:--|
| temporal: junk (where/filter) | 48 | 392–759 → 392–686 | 319–499 →
143–323 | 1 → 0 | 34 → 1 |
| temporal: junk (having) | 12 | 379–742 → 379–669 | 306–482 → 156–332 |
0 → 0 | 8 → 1 |
| temporal: year range (where/filter) | 36 | 523–703 → 422–638 | 382–600
→ 181–328 | 16 → 0 | 36 → 0 |
| temporal: year range (having) | 9 | 511–714 → 410–649 | 370–611 →
194–337 | 3 → 0 | 9 → 0 |
| temporal: misread (where/filter) | 60 | 473–903 → 445–802 | 328–643 →
156–355 | 27 → 0 | 56 → 2 |
| temporal: misread (having) | 15 | 450–913 → 422–812 | 333–653 →
169–364 | 6 → 0 | 12 → 2 |
| temporal: resolved placeholder (where/filter) | 24 | 557–826 → 497–725
| 468–652 → 235–383 | 20 → 0 | 24 → 4 |
| temporal: resolved placeholder (having) | 12 | 536–863 → 476–762 |
463–689 → 262–418 | 9 → 0 | 12 → 3 |
| text-operator door (5 types × 3 operators) | 90 | 694–839 → 693–838 |
502–617 → 152–267 | 90 → 0 | 90 → 0 |
| **all** | 306 | | | **172 → 0** | **281 → 13** |
By name size, remedy cut before → after: 1/1 87 → 0, 12/12 96 → 0, 25/31
98 → 13. The 13 left are all on the 25/31-character names; in each the
remedy starts inside the cap and only its tail (the literal-value
alternative after a placeholder's own fix, or the last alternative of
the `datetime` list behind a 60-character preview) is cut.
## Tests
- `packages/objectql/src/filter-refusal-prescription-cap.test.ts` (new,
31 cases): every refusal arm of both producers, read the way a client
reads it, through `mapDataError` (`@objectstack/types`). Each case
asserts status 400, code `INVALID_FILTER`, that the opening and closing
words of the prescription are in the bounded client text, and that they
sit before "NOT applied". Fixtures use a 12-character object and field
and paths through an aggregation filter with a nested `$and` / `$or`.
- Ablation, once: both producers restored to the `4d3a077f` blobs
(hash-checked on disk), pin run: **31 of 31 failed**; restored to `HEAD`
(hash-checked, `git diff HEAD` empty), 31 of 31 pass.
- `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2`
at `0aa08456`: 479 of 479 files, 8935 tests passed.
- `@objectstack/rest`, the 46 test files that name a temporal or text
operator or `INVALID_FILTER` (after building the closure): 46 of 46
files, 1051 passed, 289 skipped (the PostgreSQL-gated legs).
- After merging `origin/main` (`f2afe56d`; the incoming diff touches one
objectql test file): the pin, the having/where temporal door,
text-operator door and resolved-token suites, 5 files, 132 of 132.
- `pnpm --filter @objectstack/objectql typecheck`: exit 0. `eslint
--no-inline-config` on the four touched files: exit 0.
## Acceptance notes
- Not touched, by the card: the `$null` non-boolean sentence
(`rest-4xx-message-truncation.test.ts` pins that truncation on purpose).
- The card's two unlocated producers, measured with this branch's `tsx`
against the source: `no-operator-object-door.ts` json and address
variants keep the whole remedy inside the cap (remedy start 275–310, end
431–466 for a 15-character field); the single-valued lookup variant on
`aggregations[i].filter` / `having` starts its route at 329–336 but its
trailing `$in` example ends at 510–517, so the example is cut. Reported
to the seat as a finding, not changed here. `having-filter.ts`'s
cross-column class refusal carries no prescription sentence to move;
left as is.
## Verification
Derived gate families at head `f2afe56d` (`node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, change set 5 files, 252 changed lines vs
`origin/main` `ebfadb02`), each run once and its exit code captured
before any pipe. `--ran` reconciliation: **59 derived, 58 run, 1 NOT
MEASURED, 0 unrun**.
- 58 exit 0.
- `pnpm check:dual-build-cjs-loads` exit 3, PREREQUISITE NOT MET ("Run
`pnpm build` first": it needs every published package's `dist`, and only
the rest closure was built here). NOT MEASURED locally; CI runs it.
<details><summary>Every recorded command and exit code</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-engine-split-ratio.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
Every other family CI runs (the required lint, typecheck, test and build
jobs) is read from this PR's check runs, not re-run here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6Cb7UcFADbm6v5Aw1fh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(objectql): temporal-comparand and text-operator filter refusals state what to write inside the 500-character client cap
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`POST /api/v1/data/OBJECT/query` refuses a comparand a temporal field cannot read (`{ "created_date": { "$gte": "last_30_days" } }`, a year outside the field's range, an impossible day, a non-ISO spelling, a relative-date placeholder resolved outside the years) and a text operator aimed at a field that never holds a string (`{ "amount": { "$contains": "5" } }`) with `400 INVALID_FILTER`, in `where`, `aggregations[i].filter` and `having`. A client reads at most 500 characters of that message, and these refusals printed the prescription last, after the consequence, so it was often cut off. Each refusal now reads: what was written and why it is not a value of its kind, then what to write instead, then "NOT applied" and what the value would have done. The `datetime` prescription now names the relative-date placeholder first (`"{30_days_ago}"`), and the year-range prescriptions are shorter. The cap, the truncation, the code and the status are unchanged, and so is the set of filters refused.
0 commit comments