Skip to content

fix(objectql): the nested-relation condition refusal prints its route inside the client cap - #27531

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-27351-relation-refusal-route-first
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-27351-relation-refusal-route-first

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread
Fixes #27351

Clause-②: no
Metadata-protocol: none
Skills-docs: none (kind none, no narrowing arm; no skills/** file touched)

维护者速读

  • 改了什么:数据查询在 where 里拒收嵌套关联条件(第二层关联、带点的键、空对象、未声明的键、未注册的关联对象)时,报错文字改为"位置 + 简短原因 + 该怎么写 + 未执行 + 补充原因";两类拒收的开头只列条件里的第一个键并计数其余键。
  • 为什么改:客户端只读报错前 500 字符;原先"该怎么写"排在"未执行"之后,字段名稍长就被截掉(第二层关联、带点的键在普通字段名下就看不到 $in 写法);长名字下 fix(objectql): the no-operator-object refusal prints its route inside the client cap #27350 的无运算符对象拒收也仍有 6 行被截。
  • 风险与回滚:只改报错文案的顺序和措辞,拒收范围、错误码 INVALID_FILTER、状态 400、500 字符截断都不变;回滚即 revert 本 PR。
  • 先看:packages/objectql/src/relation-filter-lowering.ts(relationConditionRefusalMessage 五个分支与 describeKeys)、packages/objectql/src/no-operator-object-door.ts(describeObject)、packages/objectql/src/relation-condition-prescription-cap.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/relation-condition-prescription-cap.test.ts src/no-operator-object-prescription-cap.test.ts,预期 2 个文件 20/20 通过。
  • 需要你做的:无,席位落地。

What changes

packages/objectql/src/relation-filter-lowering.ts, relationConditionRefusalMessage: every reason now reads head, short reason, route, then "The filter was NOT applied." and any reasoning, so the route sits inside the 499 characters a client reads (CLIENT_MESSAGE_MAX, @objectstack/types, unchanged).

  • second-level: …, and 'KEY' holds a condition of its own: one level only. Read 'TARGET' with { "KEY": { … } }, then match its ids: ROUTE. The filter was NOT applied. 'KEY' is itself a TYPE field of the related object 'TARGET'.
  • dotted-key, empty, undeclared-key, unregistered-target: the route moved ahead of "NOT applied"; the reason sentence is unchanged.
  • The head names the column after the path by its type (at PATH, beneath the lookup field): the head already names the field twice (filter on 'FIELD' … at PATH.FIELD). The route names the field only in its spelling (then match its ids:, Match ids you hold:, or test for a value:).
  • The multi-valued ids route reads an $or of { "FIELD": { "$contains": ID } }, one per id (was { "FIELD": { "$contains": ID } } for one id, an $or of those for several). idsRoute is shared, so the cap refusal (relationFilterCapError) prints the same spelling.
  • The shared heads name the first key and count the rest: describeKeys here and describeObject in no-operator-object-door.ts (keys "region", and 2 more, was three keys listed). This closes the residue PR fix(objectql): the no-operator-object refusal prints its route inside the client cap #27350 recorded on the no-operator-object refusal's stress fixture, which the triage ruling asked to be measured in this PR.
  • Unchanged: the set of filters refused, INVALID_FILTER / 400, the envelope, the cap and the truncation, and the phrases other packages' tests read (names no field of the related object, 'KEY' is itself a lookup field of the related object 'X', is a dotted path: the condition reaches one level only, { "FIELD": { "$null": false } }, { "FIELD": { "$in": [ID, …] } }, The filter was NOT applied.).

Tests

  • New relation-condition-prescription-cap.test.ts: for each of the five reasons, on three fixtures, the refusal is read through mapDataError as a client reads it. The test requires 400 + INVALID_FILTER, the whole route in the client text, the route ahead of "NOT applied", and the route ending by the fixture's limit. The card's fixture (21-character object and field, 16-character target, three keys, nested $and / $or) has a limit of 459, which is 40 inside the cut. The stress fixture (27-character object and target, 32-character field, four 11- to 15-character keys, three-deep path), single- and multi-valued, has a limit of 499. That makes 15 tests.
  • no-operator-object-prescription-cap.test.ts gains a stress block: every kind at every position it is refused, on a 27-character object and target, 32-character fields, a four-key object, and aggregations[12].filter with a three-deep path. The route must end by 499.
  • Three in-surface pins were updated for the head wording. engine-nested-relation-lowering.test.ts now asserts at PATH, beneath the (lookup|tree) field, and . protocol-explicit-filter-field-gate.test.ts now asserts … at where.owner_id, beneath the lookup field,.
Package runs (head 2eb99b8, after merging origin/main f57eddc)
  • Under the verify lock: pnpm --filter '@objectstack/objectql...' build, then pnpm --filter @objectstack/objectql typecheck (tsc + check:test-typecheck OK), then pnpm --filter @objectstack/objectql test: Test Files 484 passed (484), Tests 9104 passed (9104). Then 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, with no env here; the SQLite cell ran, and it asserts the second-level, empty and undeclared words and routes on the REST body). The lock printed VERDICT command-exit 0.
  • npx eslint --no-inline-config packages/objectql: exit 0.
Ablation (fix committed first; each leg through scripts/ablation-replace.mjs)
  1. relationConditionRefusalMessage's head-and-switch span was replaced by its cf218eed1 text, which restores the old order. The anchor went from 1 to 0 and the blob from 2f1d2a495ff1 to b8031a3df929. Result: relation-condition-prescription-cap.test.ts had 15 failed (15). Examples: "the route ends at 496; the limit is 459" (the card's second-level reason); expected 303 to be greater than 420 (the card's unregistered reason, where "NOT applied" comes before the route); "the client text holds the whole route" on the stress rows. After restore the blob equals HEAD and git diff HEAD is empty.
  2. describeObject was set back to listing three keys. The anchor went from 1 to 0 and the blob from c06bf275a4ef to 4584cc487e38. Result: no-operator-object-prescription-cap.test.ts had 1 failed | 4 passed (5): the stress block, where the client text no longer holds Move the condition to 'where', or f…. After restore the blob equals HEAD and git diff HEAD is empty.

The tests import the subject 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 / narrowHavingNumberComparands and read the way a client reads it. "Before" is at cf218eed1's two producer files and "after" is this head. cut means the route ends past 499.

Nested-relation condition refusal (where)
fixture reason before: route start–end after: route start–end
card second-level 413–579 cut 266–391
card dotted-key 360–513 cut 281–393
card empty 306–463 (after "NOT applied") 245–373
card undeclared-key 347–420 (after "NOT applied") 268–341
card unregistered 327–426 (after "NOT applied") 260–327
stress second-level 492–691 cut 307–454
stress dotted-key 428–614 cut 322–456
stress empty 364–554 cut 292–442
stress undeclared-key 426–510 cut 320–404
stress unregistered 418–539 cut 312–390
stress, multi-valued second-level message 733, route after 479 cut 307–477
stress, multi-valued dotted-key message 656, route after 415 cut 322–479
stress, multi-valued unregistered message 581 cut 312–413
No-operator-object refusal, stress fixture (the #27350 residue)
kind position before after
single-valued lookup aggregations[12].filter 366–532 cut 332–498
single-valued lookup having 339–494 305–460
multi-valued lookup aggregations[12].filter 366–491 332–457
json aggregations[12].filter 352–525 cut 318–491
json having 325–498 291–464
json where 329–502 cut 295–468
address aggregations[12].filter 355–528 cut 321–494
address having 328–501 cut 294–467
address where 332–505 cut 298–471
number aggregations[12].filter 315–493 281–459
number having 288–466 254–432
number where 292–470 258–436

Acceptance notes

  • Thin margin on the extreme rows. On the no-operator stress fixture the single-valued lookup at aggregations[12].filter ends at 498, 1 inside the cut. On the relation stress fixture the multi-valued rows end at 477–479. The card-sized fixtures keep 40+ characters of margin, and so does fix(objectql): the no-operator-object refusal prints its route inside the client cap #27350's own fixture. A longer fixture still could be cut. The next lever would be the per-kind route words, not the head, which now names one key.
  • The cap refusal's multi-valued route spelling changed because idsRoute is shared. The meaning is the same, and the cap test's { "owners": { "$contains": ID } } pin still holds.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw

…ide the client cap

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
… inside the client cap

Every reason of relationConditionRefusalMessage now reads head, short reason,
route, then "NOT applied" and any reasoning. The head names the column by its
type after the path, and both shared heads (describeKeys, describeObject) name
the first key and count the rest, so the no-operator-object refusal's stress
fixture keeps its route inside the cut too.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
@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

This PR changes 1 package(s): @objectstack/objectql, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/frontend/data-source.mdx (via describeObject (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 58 of 210 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 57c51aab4d2f63293676e9412daed8a5fc54c1bc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 758592682ba496fa71ed542a2a8e37a637d1c34e — the merge of head 2eb99b8ab084ba5e002946acdae297eb03e92d72 into base 57c51aab4d2f63293676e9412daed8a5fc54c1bc, 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 758592682ba496fa71ed542a2a8e37a637d1c34e && git checkout 758592682ba496fa71ed542a2a8e37a637d1c34e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc 2eb99b8ab084ba5e002946acdae297eb03e92d72 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff 2eb99b8ab084ba5e002946acdae297eb03e92d72

node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 57c51aab4d2f63293676e9412daed8a5fc54c1bc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

维护者速读(席位终稿)

席位核对 — ACCEPT · domain:server#1 · head 2eb99b8a · 2026-10-29T11:31Z

  • Shape: 7 files, +276/−31 (307 lines). The source is +43/−28 across two producers: relation-filter-lowering.ts, which has relationConditionRefusalMessage, describeKeys and idsRoute, and no-operator-object-door.ts, which has describeObject. There are two cap pins, two in-surface wording pins and one patch changeset. check-governed-merges --pr 27531: 0 of 7 paths governed. No packages/spec, and no skills/**.
  • Premise, measured by the seat: I read the diff. Every one of the five arms now puts the route before The filter was NOT applied. The empty, dotted-key and second-level arms were the ones that printed it last. The head drops the second naming of the field, and both heads name the first key plus a count. Code, status, cap and the refused set are untouched, since only the string assembly changed. The triage ruling holds: the route comes first, with margin. The stress residue from fix(objectql): the no-operator-object refusal prints its route inside the client cap #27350 was measured here, not filed: before the change 6 rows ended at 501–532, and after it every row ends by 498. The card fixture's routes now end at 391 and 393, where they were 579 and 513.
  • Tests: the ablations through scripts/ablation-replace.mjs re-red both pins. The old order gives 15 of 15 failed, and three keys in describeObject gives 1 failed in the stress block. Both restores are byte-equal to HEAD. objectql at the head: 484 files / 9,104 tests passed.
  • Gates:
    • On 2eb99b8a: 34 success and 14 skipped, none other. check-expected-skips --pr 27531 (roster pinned to main): OK. mergeable_state: clean.
    • The dev's derived gates: 59 derived, 57 run with exit 0, 2 NOT-MEASURED on an exit 3. Those two are check:dual-build-cjs-loads, which needs a whole-repo dist, and check:type-check-debt --re-measure, which got no lock turn. CI answers both families on this head (Lint & Type Check, debt ledger): green.
  • Accepted deviations:
    • The red-first run is evidenced by the ablation legs. Two verify-lock queue timeouts blocked a pre-fix run.
    • The targeted single-file runs ran outside the lock, as declared; the full suite ran under it.
    • main moved after the one pre-PR merge, into other objectql files. It was not re-merged, per the order.
  • Declaration: Clause-②: no is right, because the wording changes and the accept/refuse result of every input does not. Metadata-protocol: none and patch fit. I read the changeset against the diff and it matches. The new multi-valued ids-route spelling is shared with the cap refusal, and the changeset says so.
  • Out of scope + Skills-docs: Skills-docs: none holds. The thin margin left on extreme stress rows (477–498) is recorded in the PR's Acceptance notes and not filed, since every row is inside the cut.
  • Landing: not governed and owes no contract review: ready, then auto-merge through the queue. 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 closes on merge (Fixes #27351).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 11:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit a1d8bf6 Oct 29, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27351-relation-refusal-route-first branch October 29, 2026 11:57
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

1 participant