Skip to content

docs(skills): objectstack-query states the served nested-relation filter in where, its limits, and the two-step route past them - #20902

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-20888-query-skill-relation-served
Sep 30, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-20888-query-skill-relation-served

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 } } in where, lowered at its filter seam (packages/objectql/src/relation-filter-lowering.ts, engine.ts lowerRelationConditions): the related object is read through the engine's own find with fields: ['id'] and limit: RELATION_FILTER_ID_CAP + 1 in the caller's execution context, and the ids become { relation: { $in: ids } } on a single-valued relation or an $or of one $contains per id on a multiple: true one. The published skill skills/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 $in route 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/main 00a92e18d:

Limit Where it lives Code / status
One level: every key a field the related object declares; a relation condition or a dotted key inside is refused before any read admitRelationCondition (second-level, dotted-key, undeclared-key, empty, unregistered-target), pinned in engine-nested-relation-lowering.test.ts INVALID_FILTER / 400, "one level only"
Forward only: the condition sits beneath a relation field of the queried object; a parent by its children is not served relation-filter-lowering.ts header ("The reverse form (a parent filtered by its children) is not served here at all") the two-step route stays
where only: an aggregation's own filter and having refuse the form no-operator-object-door.ts relationWords ("which the engine serves in 'where' and not in an aggregation's 'filter'" / "not in 'having'") INVALID_FILTER / 400
At most 1000 related ids; past that refused, never truncated RELATION_FILTER_ID_CAP = 1000, relationFilterCapError ("a cut-off id list would silently drop matching rows") INVALID_FILTER / 400
As the caller: the related object's row scope and field permissions apply; a field the caller cannot read is refused, never an empty result packages/rest/src/data-nested-relation-permission.test.ts (the security layer's filter-oracle guard) PERMISSION_DENIED / 403

Landing sites, all in skills/objectstack-query, before (00a92e18d) and after (b73023a34):

Site Before After
SKILL.md:76, Removed-key row query.joins "expand (display), or filter the related object and $in its ids" "expand (display), or { relation: { field: value } } in where (filter)"
SKILL.md:189-:191, "Filtering by a related record" the refusal (INVALID_FILTER / 400) and the two-step route served in where, 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, the search paragraph's last sentence "To filter by a related record's column, $in ids from its own query" "where: { relation: { column: value } }" (now :332)
SKILL.md:340, Cross-Object row "Filter rows by their lookup target's column" query the target, then { lookup: { $in: ids } } the served form in where up to 1000 related ids; past the cap the two-step route, $contains per id when multiple (now :341)
rules/filters.md:98-:115, ## Relation Filters the refusal, one two-step example, the multi-valued spelling, the reverse direction the served form with one ✅ example, one limits paragraph, the two-step route past the cap, the multi-valued spelling and the reverse direction unchanged

Re-read against the code and left as they are: SKILL.md:88 (the rules index, "filtering by a related record"); SKILL.md:326-:327 and :342 ("search never traverses": searchFields names 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 :26 and :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/ at 00a92e18d finds the sites above plus evals/filters-pagination-search.json:37 (a search case: the mirror field, still true) and, outside this skill, objectstack-ui/rules/list-views.md:235 and objectstack-data/SKILL.md:120 (searching by a related record's title — still true). No other sentence in skills/** 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 an os:check marker, so check:skill-examples does not apply.

Sentences for #20876 to quote

rules/filters.md ## Relation Filters, verbatim at b73023a34:

A condition on a related record's fields beneath a relation field (lookup, master_detail, user, tree) is served in where: the engine reads the related object with it as the caller, then matches the field against the ids it returns ($in; any member when multiple: true).

// ✅ Orders whose customer is in the US
where: { customer: { country: 'US' } }

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; where only: an aggregation's filter and having refuse 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 $in its ids:

const us = await engine.find('customer', { where: { country: 'US' }, fields: ['id'] });
where: { customer: { $in: us.map((c) => c.id) } }

On a multiple: true lookup match each id with $contains (an $or of those for several); the SQL driver refuses $in there. A parent by its children's fields is the same two steps reversed: query the child with the condition and fields: [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), the FilterCondition docblock form 4, and the changesets 20802-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. The where dotted path ({ 'account.industry': 'tech' }) stays refused INVALID_FIELD / 400 and its words now name the nested form; the skill teaches no dotted where path, so no sentence changes for it.

Paying for it inside rules/filters.md

The file sat at 2149 / 2149 tokens (headroom 0). The section rewrite (881 → 1463 bytes) is paid for by deleting the ## Logical Operators section (579 bytes, 41 lines), whose two rules already have a home in the same package; the file lands at 2148 / 2149, ceiling untouched.

Deleted from rules/filters.md Home
### AND (implicit) — sibling keys are AND-combined; explicit $and SKILL.md Logical Operators (:173-:181, the $and example) and this file's first Common Mistake ("sibling keys always are" AND)
### OR — $or, and $in as the one-field equivalent SKILL.md Logical Operators (:170-:171, the $or example) and $in (:123, :128); this file's Operator Reference $in row

The Common Mistake's pointer "(see Logical Operators above)" now reads "(SKILL.md, Logical Operators)". The five role: … literals in the deleted examples moved check:role-word's count for the file 7 → 2; the gate prescribes the ratchet-down ("run --update and commit the baseline"), and commit b73023a34 is that --update output: one row of scripts/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's ceil(utf8 bytes / 4))

Before (00a92e18d) After (b73023a34) Δ
SKILL.md 399 lines · 4055 tokens (ceiling 5552) 400 lines · 4109 tokens +1 line · +54 tokens
rules/filters.md 214 lines · 2149 tokens (ceiling 2149) 185 lines · 2148 tokens −29 lines · −1 token
Whole package skills/objectstack-query (6 files) 1145 lines · 10527 tokens 1117 lines · 10580 tokens −28 lines · +53 tokens
Whole catalog skills/** (every file) 13375 lines 13347 lines −28 lines

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's files[]: of the 82 tracked package.json files, 69 declare files[] and 0 entries name skills (measured at b73023a34); the catalog ships from main via npx skills add. Nothing published moves, so skip-changeset is 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 adds scripts/**; 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-word after 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/lint check:doc-formula-expressions ("22 record-scoped formula example(s) across 458 files / 1380 TS blocks judged clean", after building @objectstack/lint... under os-verify-lock.sh, VERDICT command-exit 0), spec check:skill-docs ("Skill docs in sync") and spec check:skill-refs ("9 generated files in sync"). The two ratchet families were re-run after the final commit at b73023a34 (both exit 0, verdicts as quoted).

Pins of the served form, run first-hand under os-verify-lock.sh after building each package's dependency closure (VERDICT command-exit 0 on 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 $or of $contains, the cap refusal with the two-step route, the one-level / dotted / undeclared refusals, the aggregations[i].filter and having refusals naming where, 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_URL unset 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

维护者速读(草稿)

改了什么。 发布的查询技能包 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

objectstack-fleet Bot and others added 2 commits September 30, 2026 16:32
…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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 30, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b73023a347448c931035a76a469b699593575309
Local-runs: none

Inputs read: card #20888 (body + 3 comments, incl. os-dev-report 5915796891 and triage 5914848913); PR #20902 body, file list (3 files: skills/objectstack-query/SKILL.md +7/−6, skills/objectstack-query/rules/filters.md +18/−47, scripts/role-word-baseline.json +1/−1), the API diff (Accept: application/vnd.github.diff, 6813 bytes — byte-equal to git diff 00a92e18d..b73023a34 and to the seat's pr.diff, index lines aside); the head's check-runs (two reads); ruling 5907789183 on #20802; the code PR #20872 (ca5408c62, 20 files) put on main, read at origin/main = 33b6e8bec (the PR's base.sha; merge base 00a92e18d, of which ca5408c62 is an ancestor). PR thread: 0 comments. The head is the tip of origin/claude/issue-20888-query-skill-relation-served.

① Derived judgments

The served form, statement by statement, at the code on main.

  • Mechanism sentence (rules/filters.md:59-62, SKILL.md:189-190): true. ObjectQL.lowerRelationConditions (engine.ts:11309) reads the related object through the engine's own find with fields: ['id'], limit: RELATION_FILTER_ID_CAP + 1 and context: execCtx (the caller's), then lowerRelationSite (relation-filter-lowering.ts) rewrites the site to { $in: ids } on a single-valued relation and to an $or of one { field: { $contains: String(id) } } per id when multiple: true (any member). Pinned: engine-nested-relation-lowering.test.ts ("every single-valued relation type…", "a multi-valued relation matches on ANY member…", the two-step-equality case), data-nested-object-door.test.ts ("the nested form answers exactly what the two-step route answers").
  • Relation field types (lookup, master_detail, user, tree): true. noOperatorObjectColumnKind returns 'relation' for exactly REFERENCE_VALUE_TYPES (packages/spec/src/data/field-value.zod.ts:154-156: those four); the walk's relation arm (number-comparand-declared-type-door.ts:451) admits only facts.column.kind === 'relation'. All four are pinned (engine SINGLE table; REST door rows).
  • (a) One level: true of the code, with one compression flagged in ③. admitRelationCondition refuses, before any read and structurally (the judge and execution agree): dotted-key, second-level (a key that is itself a relation field holding a no-operator object), undeclared-key, empty, unregistered-target — each through invalidFilterError → INVALID_FILTER / 400; pinned with the words (engine "keeps refusing, in the engine's words and before any read…", REST door "what the first cut keeps refusing answers 400 INVALID_FILTER… no read"). Two things the code admits that the skill's sentence does not name: a relation key compared to an id ({ owner: { account: 'acc_1' } } — one level, admitted) and the platform-provisioned id / created_at / updated_at (provisionedNoOperatorObjectColumn; the engine pin filters { owner: { id: '{current_user_id}' } }). Both are conservative omissions — an author following the sentence never writes a refused query.
  • (b) Forward only — "never a parent by its children": true. The relation arm fires only beneath a field the queried object declares as a relation; a child object's name on the parent is an undeclared filter field and takes the ordinary unknown-field refusal (INVALID_FIELD / 400, filter-comparand-shape.ts), never the relation arm. The lowering module's header states it: "The reverse form (a parent filtered by its children) is not served here at all." The kept two-step reversed route (filters.md:84-86, SKILL.md:342) is the route.
  • (c) where only: true. WHERE_SITE.servesRelations = true; AGGREGATION_FILTER_SITE and HAVING_SITE carry servesRelations = false, so at aggregations[i].filter and having the no-operator-object arm refuses with relationWords — "the nested-relation form, which the engine serves in 'where' and not in an aggregation's 'filter'" / "… not in 'having'" — INVALID_FILTER / 400 (pinned in both suites, rec.reads empty). "Served in where" holds on every door an author of this skill reaches: the engine's find / findOne / count / aggregate / update / delete and judgeFilter (pinned "covers every verb that takes a where"); POST /api/v1/data/:object/query (both REST pins; filter / $filter fold to where before the check); and an expand value's where, which expandRelatedRecords (engine.ts:10781) sends through this.find on the related object with the caller's context, so it is lowered there too.
  • (d) Cap: true. RELATION_FILTER_ID_CAP = 1000; the read asks for 1001; a matched.length past 1000 throws relationFilterCapError → INVALID_FILTER / 400, the words naming the cap, the related object and the two-step route ("a cut-off id list would silently drop matching rows"); at exactly 1000 the filter is served whole. Pinned in both suites. SKILL.md:341 "up to 1000 related ids" and filters.md:72-73 match.
  • (e) As the caller — PERMISSION_DENIED / 403, never an empty result: true of the code. The related read is a real find under execCtx through the middleware chain (engine pin: context.userId === 'u_reader', isSystem not true). The refusal is the security layer's filter-oracle guard (assertReadableQueryFields) on that read — there is no second copy in the engine — pinned with the real SecurityPlugin over SqlDriver through the REST door (data-nested-relation-permission.test.ts): 403 PERMISSION_DENIED naming the field, no records beside it, a system read filters by the value, and a related record the caller's RLS hides matches no condition (200, []). Noted, not a defect of this diff: ruling 5907789183's execution parameter spelled the loud refusal "INVALID_FILTER in the engine's words, naming the field"; the landed engine reused the security layer's existing check, so the envelope is PERMISSION_DENIED / 403 (the ruling's substance — loud, naming the field, never empty — is met). The skill states the code's envelope, as do data-engine.mdx, the objectql changeset and the FilterCondition docblock; the divergence, if the maintainer wants it named, belongs to PR feat(objectql): serve the nested-relation filter in where — lowered at the engine seam, the related object read as the caller, a loud cap, drivers untouched (#20802) #20872.

Analytics. No added or changed sentence mentions analytics, the cube read or the read scope. The three pre-existing mentions (filters.md:35, the $like allowlist; SKILL.md:52, a dataset measure filter is objectstack-ui's; SKILL.md:345) are untouched and not about the relation form. The skill's "served in where" is scoped by its own dialect table (SKILL.md:50) to the ObjectQL where, so it implies nothing about analytics. Triage 5914848913's third direction is met.

Sibling text on main. content/docs/kernel/contracts/data-engine.mdx:181-214, .changeset/20802-nested-relation-filter-served.md (objectql minor, Clause-②: yes (widening)), 20802-nested-relation-prose.md, 20802-dotted-relation-route.md, and FilterCondition form 4 (packages/spec/src/data/filter.zod.ts:1984-1999) state the same four types, the same one-level cut, the same cap and codes, the same PERMISSION_DENIED / 403, the same $in / $contains-per-id two-step route. The skill contradicts none of them; the sentences the PR body offers #20876 to quote are verbatim filters.md:59-86 at the head.

Deleted text and its homes. rules/filters.md ## Logical Operators (41 lines) is gone; at the head: implicit AND — filters.md:92-93 Common Mistake ("is an AND — sibling keys always are"); explicit $and — SKILL.md:173-181; $or — SKILL.md:170-171 and filters.md:93; $in — SKILL.md:123, :128 and filters.md:15. The one thing without a home is the example "$or of two equalities on one field ≡ $in" — an illustration, not a rule (nothing is refused or narrowed without it; both spellings stay taught). Not a lost rule. The moved cross-reference "(SKILL.md, Logical Operators)" resolves: SKILL.md:165 ### Logical Operators exists. A repo-wide grep at the head finds no mirrored copy of the rewritten or deleted sections outside skills/objectstack-query; the census skills/** shows no remaining sentence stating the refusal or the two-step route as the only way; evals/filters-pagination-search.json carries no relation-filter case (its :37 "search never traverses" case is still true).

Kept text re-judged. The two-step route (filters.md:78-81) is the exact route the cap refusal names (idsRoute); "On a multiple: true lookup match each id with $contains (an $or of those for several); the SQL driver refuses $in there" is the lowering module's own reason ("the SQL family refuses $in over the JSON column"); the reverse two steps and SKILL.md:342 stay true. Triage direction 5914848913 met in full: the served form and its five limits, the two-step route kept for past the cap and for the reverse form, nothing on analytics.

② Semver level

skip-changeset is right and Clause-②: no is right. Reproduced at the head: 82 tracked package.json, 69 with files[], 0 entries naming skills or scripts — skills/** and scripts/role-word-baseline.json ship in no released package, so nothing published moves. No type, schema or export is touched; the contract widening was PR #20872's own Clause-②: yes (widening) changeset. The skip-changeset label landed 16:56:31Z (the seat); Check Changeset has two runs — 16:56:34Z skipped (pre-label event, not a failure) and 16:57:49Z success (post-label). No stale failure exists.

③ Boundary flags

  • scripts/role-word-baseline.json, rules/filters.md 7 → 2 (dev deviation 1) — outside the claim's declared surface (claim 5915190257 named the two skill files, the ratchet script only if a ceiling lowered, and content/docs/** read-only). Judged the gate's own ratchet-down and nothing more: check-role-word.mjs fails on "a baselined file's count DECREASED … run with --update to ratchet the baseline down and commit it" and states "ratcheting down is the author's own remedy" (only expansion is ⛔ MAINTAINER-ONLY). The count reproduces (whole-word role: 7 at 00a92e18d, 2 at the head — the five role: literals in the deleted examples). --update rewrites the whole baseline from the tree, and the diff moves exactly this one row, so no other file drifted. Answered; declared in the PR body and the report.
  • Dev deviation 2 (commit identity): e13345bdb authored as objectstack-fleet[bot], b73023a34 as "Claude" with the Claude-Session + Co-authored-by trailers, both on the claimed branch. Cosmetic; noted.
  • Dev deviation 3 (built dist before the lint/spec gates): a property of the dev's local run, no bearing on the diff.
  • Readings in the PR body, reproduced from the head with wc and ceil(bytes/4) (the ratchet's convention): SKILL.md 399 → 400 lines, 16219 → 16436 bytes = 4055 → 4109 tokens (ceiling 5552 held); rules/filters.md 214 → 185 lines, 8594 → 8590 bytes = 2149 → 2148 tokens (ceiling 2149 held, headroom 0 → 1); package 6 files, 1145 → 1117 lines, 10527 → 10580 tokens; catalog skills/** 13375 → 13347 lines (−28 against a +4 budget). No ceiling row moved (check-skills-token-ratchet.mjs untouched); no untouched line was re-wrapped (the diff is hunk-local).
  • Precision items for the maintainer's landing hand (Tier H) — none blocking, none false under the sentence's evident reading:
    1. filters.md:69-70 "no relation or dotted key inside" compresses the code's "not itself a relation holding a condition of its own"; read literally it withholds { order: { customer: 'c_1' } }, which is served. The sibling docblock's spelling ("a relation condition beneath it, or a dotted key, is refused") is the exact one; the same sentence also omits the provisioned id / created_at / updated_at. filters.md has 1 token of headroom, so a tightening must be paid in-file.
    2. SKILL.md:88 (rules index) still promises filters.md covers "logical combinations"; after the deletion the file's only such text is the Common Mistake. SKILL.md has 1443 tokens of headroom.
    3. compatibility: Requires @objectstack/spec 17.x is left as the dev notes: the catalog states main's behaviour while the published 17.5.0 engine still refuses the form — the ruling's sequencing, not this card's.
  • Check-runs on the head (read 1, ~16:58Z: 34 runs — 21 success, 11 skipped, 2 in_progress; read 2, 17:06:05Z, and read 3, 17:09:07Z, identical: 35 runs — 23 success, 11 skipped, 1 in_progress). Not success / skipped at reads 2 and 3: Lint & Repo Gates — in_progress (started 16:51:46Z), the job that carries check-skills-token-ratchet, check:role-word and the other repo gates this diff derives; an honest reading, not a pass — it must complete green before the landing precondition "every check green" holds. Test Core (1/6), in_progress at read 1, completed success. The 11 skipped: Auto Label, Check PR Size, Check Changeset (each a superseded duplicate-event run whose twin is success), Build Core, Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix placeholder), Dogfood Verify CLI, Packed-tarball smoke (opt-in) ×2, Temporal Conformance (live PG + MySQL) — path-filtered or opt-in on a docs-plus-baseline diff. No failure, no cancelled, no action_required. The dev's local gate runs (31 derived families, all exit 0, per the report) are the dev's testimony; the check-run is the gate verdict of record.

Implemented-by: claude/issue-20888-query-skill-relation-served
Reviewed-by: session_01KTZmMfzVzjNvyaLyQ8mHvg

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #20902 · 查询技能:关联字段筛选改教引擎已支持的写法(#20888)

skills 席 1 · session_01KTZmMfzVzjNvyaLyQ8mHvg · 2026-09-30T17:22Z · 所审 head b73023a3

改了什么:改的是 skills/objectstack-query 下两个文件,另加 scripts/role-word-baseline.json 一行;技能包净 −28 行。

为什么改:您在 #20802 裁 A(「20802 同意」):v18 支持这种写法,技能「同一轮更新」。引擎这一半由 PR #20872 落地后,技能仍教「被拒绝」,AI 会照旧手写两步路线。

风险与代价(含回滚):只改文档,不发布任何包,回滚方式是 revert。需要您知道的一点:技能 compatibility 仍写 @objectstack/spec 17.x,但正文描述的是 main 上的引擎;已发布的 17.x 引擎仍拒绝这种写法。这是裁决定的时序(技能与 v18 同轮),本 PR 没有改动它。

席位意见:ACCEPT,建议批准。

  • 契约复核 PASS(评论 5916062797),由隔离的 fable 子代理出具,五条限制都在代码层逐条核实。
  • 复核指出两处措辞偏保守,都不会让 AI 写出会被拒绝的查询,席位按原样落地、记为观察:
    • 「内部不能再有关联键」,其实拿关联键跟 id 比较是支持的;
    • SKILL.md 规则索引还写着「logical combinations」。
  • CI 全绿。

你要做的:审阅后给 APPROVED,之后由本席位落地。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 30, 2026 23:08
@os-zhuang
os-zhuang enabled auto-merge September 30, 2026 23:08
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 4957ee5 Sep 30, 2026
46 of 47 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-20888-query-skill-relation-served branch September 30, 2026 23:34
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants