docs(skills): objectstack-query states the served nested-relation filter in where, its limits, and the two-step route past them - #20902
Conversation
…ter, its limits, and the two-step route past them (WIP) Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
…d (7 → 2), as the gate prescribes Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #20888 (body + 3 comments, incl. ① Derived judgmentsThe served form, statement by statement, at the code on
Analytics. No added or changed sentence mentions analytics, the cube read or the read scope. The three pre-existing mentions ( Sibling text on Deleted text and its homes. Kept text re-judged. The two-step route ( ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)— PR #20902 · 查询技能:关联字段筛选改教引擎已支持的写法(#20888)skills 席 1 · 改了什么:改的是
为什么改:您在 #20802 裁 A(「20802 同意」):v18 支持这种写法,技能「同一轮更新」。引擎这一半由 PR #20872 落地后,技能仍教「被拒绝」,AI 会照旧手写两步路线。 风险与代价(含回滚):只改文档,不发布任何包,回滚方式是 revert。需要您知道的一点:技能 席位意见:ACCEPT,建议批准。
你要做的:审阅后给 APPROVED,之后由本席位落地。 Generated by Claude Code |
Fixes #20888
Clause-②: no
What this changes
Since PR #20872 (
ca5408c62, the engine half of ruling 5907789183 on #20802) the engine serves{ relation: { field: value } }inwhere, lowered at its filter seam (packages/objectql/src/relation-filter-lowering.ts,engine.tslowerRelationConditions): the related object is read through the engine's ownfindwithfields: ['id']andlimit: RELATION_FILTER_ID_CAP + 1in the caller's execution context, and the ids become{ relation: { $in: ids } }on a single-valued relation or an$orof one$containsper id on amultiple: trueone. The published skillskills/objectstack-query, rewritten by PR #20811 (1d202453) the same day and before that landing, still told AI authors the form is refused (INVALID_FILTER/ 400) and that the two-step$inroute is the only way. This PR states the served form, its limits and the two-step route past them, at every site PR #20811 rewrote — and nothing about analytics, which #20887 carries.Every limit is stated from the code and its pins, read at
origin/main00a92e18d:admitRelationCondition(second-level,dotted-key,undeclared-key,empty,unregistered-target), pinned inengine-nested-relation-lowering.test.tsINVALID_FILTER/ 400, "one level only"relation-filter-lowering.tsheader ("The reverse form (a parent filtered by its children) is not served here at all")whereonly: an aggregation's ownfilterandhavingrefuse the formno-operator-object-door.tsrelationWords("which the engine serves in 'where' and not in an aggregation's 'filter'" / "not in 'having'")INVALID_FILTER/ 400RELATION_FILTER_ID_CAP = 1000,relationFilterCapError("a cut-off id list would silently drop matching rows")INVALID_FILTER/ 400packages/rest/src/data-nested-relation-permission.test.ts(the security layer's filter-oracle guard)PERMISSION_DENIED/ 403Landing sites, all in
skills/objectstack-query, before (00a92e18d) and after (b73023a34):SKILL.md:76, Removed-key rowquery.joinsexpand(display), or filter the related object and$inits ids"expand(display), or{ relation: { field: value } }inwhere(filter)"SKILL.md:189-:191, "Filtering by a related record"INVALID_FILTER/ 400) and the two-step routewhere, read as the caller; the five limits by name and the two-step route past them, by pointer to the rule (now:189-:192)SKILL.md:331, thesearchparagraph's last sentence$inids from its own query"where: { relation: { column: value } }" (now:332)SKILL.md:340, Cross-Object row "Filter rows by their lookup target's column"{ lookup: { $in: ids } }whereup to 1000 related ids; past the cap the two-step route,$containsper id whenmultiple(now:341)rules/filters.md:98-:115,## Relation FiltersRe-read against the code and left as they are:
SKILL.md:88(the rules index, "filtering by a related record");SKILL.md:326-:327and:342("searchnever traverses":searchFieldsnames are judged exactly at the ingress,protocol.ts"Names are judged EXACTLY (no dotted-head tolerance)", so a dotted name is still refused and the mirror-field route still holds);SKILL.md:341"Filter parent by child conditions" (the reverse form, not served, stays two-step). The same-day rows:26and:38-:44(PR #20776) are untouched.Census.
grep -rn -i -E 'nested.relation|two steps|two-step|\$in its ids|never traverses|relation.*INVALID_FILTER|filter the related object' skills/at00a92e18dfinds the sites above plusevals/filters-pagination-search.json:37(asearchcase: the mirror field, still true) and, outside this skill,objectstack-ui/rules/list-views.md:235andobjectstack-data/SKILL.md:120(searching by a related record's title — still true). No other sentence inskills/**states the refusal or the two-step route as the only way. The evals carry no relation-filter case, so nothing there asserts the refusal; no block in the skill carries anos:checkmarker, socheck:skill-examplesdoes not apply.Sentences for #20876 to quote
rules/filters.md## Relation Filters, verbatim atb73023a34:A condition on a related record's fields beneath a relation field (
lookup,master_detail,user,tree) is served inwhere: the engine reads the related object with it as the caller, then matches the field against the ids it returns ($in; any member whenmultiple: true).Limits — one level: every key a field the related object declares, no relation or dotted key inside; forward only: never a parent by its children;
whereonly: an aggregation'sfilterandhavingrefuse it,INVALID_FILTER/ 400; at most 1000 related ids, refused past that,INVALID_FILTER/ 400, never truncated; as the caller: the related object's row scope and field permissions apply, so a field the caller cannot read is refused,PERMISSION_DENIED/ 403, never an empty result. Past the cap, run the two steps yourself — filter the related object, then$inits ids:On a
multiple: truelookup match each id with$contains(an$orof those for several); the SQL driver refuses$inthere. A parent by its children's fields is the same two steps reversed: query the child with the condition andfields: [the lookup], then{ id: { $in: … } }on the parent.These sentences agree with the first landed wording of the fact —
content/docs/kernel/contracts/data-engine.mdx(PR #20872), theFilterConditiondocblock form 4, and the changesets20802-nested-relation-filter-served.md/20802-dotted-relation-route.md/20802-nested-relation-prose.md— each read against the code; none contradicts it, and the skill contradicts none of them. Thewheredotted path ({ 'account.industry': 'tech' }) stays refusedINVALID_FIELD/ 400 and its words now name the nested form; the skill teaches no dottedwherepath, so no sentence changes for it.Paying for it inside
rules/filters.mdThe file sat at 2149 / 2149 tokens (headroom 0). The section rewrite (881 → 1463 bytes) is paid for by deleting the
## Logical Operatorssection (579 bytes, 41 lines), whose two rules already have a home in the same package; the file lands at 2148 / 2149, ceiling untouched.rules/filters.md### AND (implicit)— sibling keys are AND-combined; explicit$andSKILL.mdLogical Operators (:173-:181, the$andexample) and this file's first Common Mistake ("sibling keys always are" AND)### OR—$or, and$inas the one-field equivalentSKILL.mdLogical Operators (:170-:171, the$orexample) and$in(:123,:128); this file's Operator Reference$inrowThe Common Mistake's pointer "(see Logical Operators above)" now reads "(SKILL.md, Logical Operators)". The five
role: …literals in the deleted examples movedcheck:role-word's count for the file 7 → 2; the gate prescribes the ratchet-down ("run--updateand commit the baseline"), and commitb73023a34is that--updateoutput: one row ofscripts/role-word-baseline.json. That file is outside the claim's declared surface; declared here and in the report.skills/**readings (lines and tokens; tokens are the ratchet'sceil(utf8 bytes / 4))00a92e18d)b73023a34)SKILL.mdrules/filters.mdskills/objectstack-query(6 files)skills/**(every file)Line budget (PM-set, net +4 at most): −28. No untouched line was re-wrapped; no ceiling row moved.
Changeset.
skills/**ships in no package'sfiles[]: of the 82 trackedpackage.jsonfiles, 69 declarefiles[]and 0 entries nameskills(measured atb73023a34); the catalog ships frommainvianpx skills add. Nothing published moves, soskip-changesetis the seat's to apply; this PR writes no label.Verification
Gates at head
b73023a34, each exit code captured by redirect and the verdict line quoted from its log:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; "change set derived from git — 3 path(s) vs merge base 00a92e1") derives 31 commands, six families more than the dispatch's list because the baseline row addsscripts/**; reconciled with--ran: "31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero — all 31 recorded an exit code and none of them is 3)". Every one exit 0:check-skills-token-ratchet("54 authored bundle file(s) within their ceilings") and its--self-test(65 cases),check:role-wordafter the ratchet-down ("Ledger: 44 baselined file(s) still carrying it (117 occurrence(s))"),check:corpus-claim-drift,check:skill-identifier-liveness("Leg 1: 457 citation(s) over 53 published file(s)"),check:doc-authoring,check:nul-bytes,check:skill-compatibility,check:skill-frame-sync,check:agent-test-spelling,check:cross-package-test-inputs,check:driver-memory-census,check:gitlink-declared,check:pm-governed-merges(the--self-test, as the script spells it),check:refd-timer-probe,check:watch-hint-literal,check-ci-filter-parity(+--self-test),check-closing-keyword-parity(+--self-test),check-comment-mask-corpus("7664 files, 0 disagree"),check-doc-route-spelling --advisory(+--self-test),check-scripts-symbol-anchors(+--self-test),check:bash32-floor,check:cli-command-ids,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,@objectstack/lintcheck:doc-formula-expressions("22 record-scoped formula example(s) across 458 files / 1380 TS blocks judged clean", after building@objectstack/lint...underos-verify-lock.sh,VERDICT command-exit 0), speccheck:skill-docs("Skill docs in sync") and speccheck:skill-refs("9 generated files in sync"). The two ratchet families were re-run after the final commit atb73023a34(both exit 0, verdicts as quoted).Pins of the served form, run first-hand under
os-verify-lock.shafter building each package's dependency closure (VERDICT command-exit 0on both):pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-nested-relation-lowering.test.ts→ "Test Files 1 passed (1), Tests 13 passed (13)" (the served rows on every relation type, the any-member$orof$contains, the cap refusal with the two-step route, the one-level / dotted / undeclared refusals, theaggregations[i].filterandhavingrefusals namingwhere, the caller's context on the related read);pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/data-nested-relation-permission.test.ts src/data-nested-object-door.test.ts→ "Test Files 2 passed (2), Tests 9 passed | 12 skipped (21)" — the 12 are the PostgreSQL and MySQL cells of the door test, named skips (OS_TEST_POSTGRES_URL/OS_TEST_MYSQL_URLunset here); the memory and SQLite cells and the 403 pin ran. No package is touched, so no package build or test suite is owed beyond that.Acceptance notes
SKILL.md'scompatibility: Requires @objectstack/spec 17.xis left as is: the catalog statesmain's behaviour (as PR docs(skills): objectstack-query teaches the served route for a related record's column, not the refused nested form #20811 did) andmainis 17.5.0 with the served form; the published 17.5.0 engine still refuses it, and which release carriesca5408c62is the ruling's sequencing, not this card's. Noted, not filed.domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887 is open, and the sentences above namewhereon the engine and the data door only.content/docs/protocol/objectql/query-syntax.mdxis read-only here; [finding] docs: query-syntax.mdx "Filtering Across Relationships" says SqlDriver compiles a nested relation object and emits a dotted key to Knex — both are refused at the engine before any driver since 4b4ee88f #20876 carries it and quotes the section above.api_writeslists every relay write of this run.维护者速读(草稿)
改了什么。 发布的查询技能包
skills/objectstack-query今天早些时候(PR #20811)被改成「引擎拒绝在关联字段下直接写条件」;同一天晚些时候引擎侧 PR #20872 落地,引擎已经支持这种写法(where: { customer: { country: 'US' } })。本 PR 把同样的几处句子改成引擎现在的真实行为:支持,以及四条边界(只到一层、只能正向、只在where里、关联 id 最多 1000 条超出即拒绝),并且以调用者身份读关联对象(读不到的字段报 403,不会静默给空结果);超过上限或反向(用子记录条件筛父记录)仍走原来的两步$in。规则文件里删掉了与 SKILL.md 重复的一节逻辑运算符示例,用来支付这段改写;整个包净减 28 行,每个文件的 token 上限都没动。为什么改。 技能包是客户项目里 AI 的教材。裁决 5907789183(「20802 同意」)明确写了技能包在同一轮更新;不改,AI 作者会照着旧句子手写两步查询,正是裁决要去掉的模式。措辞与已落地的文档页(
data-engine.mdx)、三份 changeset 和FilterCondition的注释逐句核对过,一致;#20876 改query-syntax.mdx时引用本 PR 的句子,两处不会分叉。风险与代价(含回滚)。 只改两份 markdown 与一行门禁基线(
role-word计数 7 → 2,门禁自己要求的下调),不改任何代码或发布包。风险在两点:一是措辞若与后续分析(#20887)落地后的行为有出入,分析那一半另改;二是已发布的 17.5.0 引擎仍拒绝这种写法,技能包描述的是main。回滚即 revert 本 PR 的两个提交。席位意见。
你要做的。 复核五条边界的措辞与「逻辑运算符」一节的删除归宿;批准后由席位落地(Tier H)。
Generated by Claude Code