Skip to content

fix(objectql): the no-operator-object refusal prints its route inside the client cap - #27350

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-cgmcnw
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-cgmcnw

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Requested by Bill · project thread
Fixes #27337

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:数据查询拒收"没有 $ 运算符的对象"时,报错文字改为先说"该怎么写",再说"未执行"和原因;四类字段(单值/多值 lookup、json/address、普通标量)都按此顺序。
  • 为什么改:客户端只读报错前 500 字符;原先修复建议写在最后,字段名稍长就被截掉,单值 lookup 在聚合 filter / having 上即使 15 字符字段也看不到 $in 示例。
  • 风险与回滚:只改报错文案顺序与少量措辞,拒收范围、错误码 INVALID_FILTER、状态 400、截断上限都不变;回滚即 revert 本 PR。
  • 先看:packages/objectql/src/no-operator-object-door.ts(四个 words 函数)、packages/objectql/src/no-operator-object-prescription-cap.test.ts(按客户端读法逐类钉住)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/no-operator-object-prescription-cap.test.ts,预期 4/4 通过。
  • 需要你做的:无,席位落地。

What changes

packages/objectql/src/no-operator-object-door.ts — every kind of the no-operator-object refusal now reads verdict, route, then "NOT applied" and the reasoning, so the route sits inside the 499 characters a client reads (CLIENT_MESSAGE_MAX, @objectstack/types, unchanged):

  • Single-valued lookup (aggregations[i].filter, having) — the card's arm. The $in route moved ahead of "NOT applied"; it opens "Move the condition to 'where'" and names the related object by its name alone (filter 'TARGET' first and match the ids it returns: { "FIELD": { "$in": [ID, …] } }.).
  • Multi-valued lookup, json / address, scalar — measured past the cut too with a long field name (tables below), so fixed in the same PR, as the dispatch asked: each route moved ahead of "NOT applied".
  • The verdict names the column by its type (a lookup field, an aggregated lookup column, a platform-provisioned text column): the head already names the field twice (filter on 'FIELD' … at PATH.FIELD). The full name (the declared lookup field 'FIELD', the aggregated column 'FIELD', which carries a … value) moved into the reasoning after "NOT applied".
  • Unchanged: the set of filters refused, INVALID_FILTER / 400, the envelope, the cap and the truncation, and every phrase another package's test reads (which the engine serves in 'where', The filter was NOT applied., is filter structure, not a value, Compare 'id' with a value ({ "id": VALUE }), { "meta": { "$null": false } }, or store the part you filter on in a field of its own).

Tests: a new no-operator-object-prescription-cap.test.ts reads each kind through mapDataError, as a client does, with a long fixture (21-character object, 20- to 24-character fields, a three-key object, a path through an aggregation's filter and a nested $and / $or), and requires the whole route in the client text, ending at least 40 characters before the cut, ahead of "NOT applied". Measured route ends on that fixture: 368–444 (margin 55–131). Two in-surface tests updated for the new verdict wording (engine-nested-object-door.test.ts, engine-nested-relation-lowering.test.ts). One patch changeset for @objectstack/objectql.

Tests

Targeted and package runs (head 885fc60)
  • pnpm --filter @objectstack/objectql test — Test Files 479 passed (479), Tests 8942 passed (8942); then check:test-typecheck green (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/objectql typecheck — exit 0, after pnpm --filter @objectstack/objectql build.
  • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/data-nested-object-door.test.ts src/data-no-operator-object-door.test.ts — 10 passed, 20 skipped (the live PostgreSQL / MySQL cells, no env here; the SQLite cell ran).
  • eslint --no-inline-config packages/objectql — exit 0.
Ablation, once (the fix committed first)

no-operator-object-door.ts replaced by its origin/main 6d7634a9 blob (hash 4ebc814b confirmed on disk; the new wording absent, the old present): no-operator-object-prescription-cap.test.ts → 4 failed (4), e.g. "the route ends at 488 … expected 488 to be less than or equal to 459". Restored with git checkout HEAD --: hash 58f30588 equals the HEAD blob, git diff HEAD empty, 4 passed (4). The subject is imported by a relative src path, so no dist build sits between the test and the edit.

Verification — route offsets before and after

Measured with tsx through narrowNumberComparands (aggregations[12].filter, nested $and / $or) and narrowHavingNumberComparands, read through mapDataError. "Field chars" counts the field name; object customer_support_case and target customer_account for the first table; "2 keys" is city / zip, "4 keys" is four 9- to 15-character keys. A multi-valued lookup at having has no row: an aggregated column has no declaration, so it gets the single-valued words.

Long field (24 chars): before, every arm cut; after, every arm inside
field chars object arm position before: route start–end after: route start–end
24 2 keys single-valued lookup aggregations[i].filter 388–596 cut 290–437 inside
24 2 keys single-valued lookup having 365–554 cut 271–418 inside
24 2 keys multi-valued lookup aggregations[i].filter 388–520 cut 290–415 inside
24 2 keys json aggregations[i].filter 349–514 cut 276–441 inside
24 2 keys json having 343–508 cut 257–422 inside
24 2 keys address aggregations[i].filter 352–517 cut 279–444 inside
24 2 keys address having 346–511 cut 260–425 inside
24 2 keys number (scalar) aggregations[i].filter 370–524 cut 239–393 inside
24 2 keys number (scalar) having 364–518 cut 220–374 inside
24 4 keys single-valued lookup aggregations[i].filter 438–646 cut 340–487 inside
24 4 keys single-valued lookup having 415–604 cut 321–468 inside
24 4 keys multi-valued lookup aggregations[i].filter 438–570 cut 340–465 inside
24 4 keys json aggregations[i].filter 399–564 cut 326–491 inside
24 4 keys json having 393–558 cut 307–472 inside
24 4 keys address aggregations[i].filter 402–567 cut 329–494 inside
24 4 keys address having 396–561 cut 310–475 inside
24 4 keys number (scalar) aggregations[i].filter 420–574 cut 289–443 inside
24 4 keys number (scalar) having 414–568 cut 270–424 inside
Stress: 27-character object and target, 18- and 32-character fields
field chars object arm position before: route start–end after: route start–end
18 2 keys single-valued lookup aggregations[i].filter 376–583 cut 284–436 inside
18 2 keys single-valued lookup having 353–530 cut 265–406 inside
18 2 keys multi-valued lookup aggregations[i].filter 376–508 cut 284–409 inside
18 2 keys json aggregations[i].filter 337–496 inside 270–429 inside
18 2 keys json having 331–490 inside 251–410 inside
18 2 keys address aggregations[i].filter 340–499 inside 273–432 inside
18 2 keys address having 334–493 inside 254–413 inside
18 2 keys number (scalar) aggregations[i].filter 358–494 inside 233–369 inside
18 2 keys number (scalar) having 352–488 inside 214–350 inside
18 4 keys single-valued lookup aggregations[i].filter 426–633 cut 334–486 inside
18 4 keys single-valued lookup having 403–580 cut 315–456 inside
18 4 keys multi-valued lookup aggregations[i].filter 426–558 cut 334–459 inside
18 4 keys json aggregations[i].filter 387–546 cut 320–479 inside
18 4 keys json having 381–540 cut 301–460 inside
18 4 keys address aggregations[i].filter 390–549 cut 323–482 inside
18 4 keys address having 384–543 cut 304–463 inside
18 4 keys number (scalar) aggregations[i].filter 408–544 cut 283–419 inside
18 4 keys number (scalar) having 402–538 cut 264–400 inside
32 2 keys single-valued lookup aggregations[i].filter 418–653 cut 312–478 inside
32 2 keys single-valued lookup having 395–600 cut 293–448 inside
32 2 keys multi-valued lookup aggregations[i].filter 418–550 cut 312–437 inside
32 2 keys json aggregations[i].filter 379–552 cut 298–471 inside
32 2 keys json having 373–546 cut 279–452 inside
32 2 keys address aggregations[i].filter 382–555 cut 301–474 inside
32 2 keys address having 376–549 cut 282–455 inside
32 2 keys number (scalar) aggregations[i].filter 400–578 cut 261–439 inside
32 2 keys number (scalar) having 394–572 cut 242–420 inside
32 4 keys single-valued lookup aggregations[i].filter 468–703 cut 362–528 cut
32 4 keys single-valued lookup having 445–650 cut 343–498 inside
32 4 keys multi-valued lookup aggregations[i].filter 468–600 cut 362–487 inside
32 4 keys json aggregations[i].filter 429–602 cut 348–521 cut
32 4 keys json having 423–596 cut 329–502 cut
32 4 keys address aggregations[i].filter 432–605 cut 351–524 cut
32 4 keys address having 426–599 cut 332–505 cut
32 4 keys number (scalar) aggregations[i].filter 450–628 cut 311–489 inside
32 4 keys number (scalar) having 444–622 cut 292–470 inside

Acceptance notes

  • Residual, stress fixture only: with a 27-character object, a 32-character field, a 27-character target, four 9- to 15-character keys and a three-deep path, five rows still end past the cut (single-valued lookup and json / address in aggregations[i].filter; json / address at having, 502–528). There the head alone (context, field, keys, path) takes about 270 characters before any arm's words; closing it means shortening the shared head (describeObject shows three keys), which every position and arm prints. Not done here; recorded for the seat.
  • Neighbour, other producer (out of this PR's surface): relationConditionRefusalMessage in packages/objectql/src/relation-filter-lowering.ts (the where refusals of a nested-relation condition) prints its route LAST. Measured with a 21-character field, a 16-character target, three keys and a nested path: second-level 608 characters, dotted-key 527, empty 508 — each route ends past the 499 cut. Reported to the seat in the dev report.

Generated by Claude Code

… the client cap

Every kind of the no-operator-object filter refusal (scalar, structured-JSON,
single- and multi-valued relation) now reads verdict, route, then "NOT
applied" and the reasoning, so the route lands inside the 500 characters a
client reads. The verdict names the column by its type, since the head
already names the field twice. The cap, truncation, code and status are
unchanged.

Claude-Session: https://claude.ai/code/session_01Utp6CXc5hequf76uJfzrdK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 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
  • the SDK route bridge reached 56 of 208 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73725c206f55580db058645dbffe68280b095ef6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 549d3e90c0f8fad9fb2006302b7089ec6fef74c7 — the merge of head 885fc60787fb0943b6b875b6b74866bb07cec8cd into base 73725c206f55580db058645dbffe68280b095ef6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 549d3e90c0f8fad9fb2006302b7089ec6fef74c7 && git checkout 549d3e90c0f8fad9fb2006302b7089ec6fef74c7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 73725c206f55580db058645dbffe68280b095ef6 885fc60787fb0943b6b875b6b74866bb07cec8cd && git checkout -B drift-repro 73725c206f55580db058645dbffe68280b095ef6 && git merge --no-ff 885fc60787fb0943b6b875b6b74866bb07cec8cd

node scripts/docs-audit/affected-docs.mjs --json 73725c206f55580db058645dbffe68280b095ef6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

席位核对 — ACCEPT · domain:server#3 · session_01Utp6CXc5hequf76uJfzrdK · head 885fc607 · 2026-10-29T02:32Z

  • Shape: 5 files, +177/−28. They are packages/objectql/src/no-operator-object-door.ts (the four *Words builders and describeColumn), a new pin no-operator-object-prescription-cap.test.ts, two in-surface wording updates in tests, and one patch changeset. There is no governed surface, no packages/spec and no engine.ts (seat 2's serial queue), and it is under the 3,000-line threshold.
  • The ruling holds in the diff: triage 6484973815. The prescription comes first at the producer, with margin inside the cap. The cap, the truncation, INVALID_FILTER/400, the envelope and the refused set are unchanged, and every phrase another package's test reads is kept.
  • Scope widened inside the declared file, as the order directed. With a long field name, the multi-valued lookup, json/address and scalar kinds were also past the cut. The PR carries before/after tables for every arm.
  • Tests read. The pin reads each kind through mapDataError, as a client does, with a long fixture, and requires the whole route to end ≥ 40 characters before the cut and ahead of "NOT applied". The ablation against the base blob went red 4/4 and was then restored.
  • Declaration: Clause-②: no and Metadata-protocol: none, which is message text only, at patch. No contract-review face (reviewFace: no hits).
  • Gates: every check suite on 885fc607 completed with no failure; mergeable_state: clean.
  • Residual and neighbour. A stress-fixture residual remains: the shared head is about 270 characters, and 5 rows end at 502–528 with 32-character names and 4 keys. That is recorded in the PR's Acceptance notes. The same class on relation-filter-lowering.ts was filed as data: the nested-relation condition refusal prints its route last, past the 500-character client cap, for a second-level relation, a dotted key and an empty object in where #27351.
  • Landing: ready → auto-merge through the queue.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 02:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 02:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 8d66cbe Oct 29, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-cgmcnw branch October 29, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants