skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens) - #14443

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-14302-skills-query-optimization
Sep 2, 2026

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #14302 — the eval items (QRY-D-05 / H-02 / H-03) are deferred to #14296, so that card stays open.

Member of the skills catalog optimization program #14292. Governed surface (skills/**) ⇒ this PR stays DRAFT; needs:contract-review is on this PR and on #14302.

Audit record: posted verbatim as a comment on #14302 (read back byte-identical). It is the finding-by-finding spec this PR implements.

Head sha 49de20f (review round 1 + the role-word baseline ratchet, then merged with current main).

Files outside the declared surface

One file beyond the four authored skill files and the one generated doc:

filewhyprescribed by
scripts/role-word-baseline.jsoncheck:role-word reds on improvement as well as regression. Round 2 deleted two Common-Mistakes blocks in rules/filters.md, removing one baselined occurrence, and the gate refused: role-word count improved 9 → 8 — ratchet DOWN: run node scripts/check-role-word.mjs --update and commit the baseline.the gate's own message. --update moved exactly one row (skills/objectstack-query/rules/filters.md: 9 → 8) and no other, verified by git diff before committing; it is its own commit, 8208cf30. The data and api flights took the same path.

No ceiling was changed, in this file or any other.

Review round 1 — both FAIL spans, measured and fixed

Round 1 passed everything except two spans. Both were re-measured at source before being touched; one was a real defect, one turned out to be correct as written and is now stated more precisely.

1. ENGINE_FIND_OPTION_KEYS was under-reported — REAL DEFECT, fixed

The calling-convention table listed nine keys for engine find/findOne and the prose called the set closed. packages/objectql/src/engine.ts:396-400 spreads ENGINE_DRIVER_PASSTHROUGH_KEYS into it, and that constant (:368-370) is transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit. As written the table therefore told a reader that an explicit tenantId is refused by name, which is false.

Fixed: the find/findOne row now names all fifteen keys, and the prose carries the reason the source itself gives at :360-366 — on find/findOne (and update/delete) the option bag IS the base of the driver options, which is how an explicit tenantId reaches the driver; count and aggregate never forward the bag, so on those two the same keys are deliberately ILLEGAL. The count (context, where) and aggregate (six keys, no passthrough) rows are unchanged — both were already exact, and the source comment says the omission there is deliberate.

2. The $like/$ilike face list — VERIFIED AT SOURCE, kept and sharpened

Review offered two routes: verify each named face with file:line and the exact message, or reduce the sentence. Taking the first route: all three refusals hold. Each is an allowlist miss, which is why grepping the drivers for the literal $like does not find them.

faceallowlist that omits themrefusal sitecode / statusmessage
driver-mongodb— (explicit default: arm)packages/drivers/driver-mongodb/src/mongodb-filter.ts:1071-1074unsupportedFilterError at :366-371 sets INVALID_FILTER / 400Unsupported filter operator "$like" on field … It is refused rather than passed through to MongoDB
objectql havingpackages/objectql/src/having-filter.ts:136-141CONDITION_OPERATORS, sixteen operators, no $like/$ilikeunknownOperator at :174-205invalidFilterErrorINVALID_FILTER / 400 (the doc table at :157-158 enumerates the faces sharing that envelope)Unsupported operator '$like' in having. … An unknown operator is refused rather than ignored
service-analyticspackages/services/service-analytics/src/strategies/filter-normalizer.ts:418-435MONGO_TO_CUBE_OP, no $like/$ilikemiss branch at :1032-1046invalidFilterError at :403-406 sets INVALID_FILTER / 400[analytics] Unsupported filter operator "$like" on … Dropping it would silently widen the query

packages/spec/src/data/filter.zod.ts:697 states exactly this, so the spec .describe() is accurate and no spec-side twin card is owed.

Two readings in the review did not survive measurement, so the sentence is corrected rather than weakened:

  • driver-memory ANSWERS $like/$ilike.packages/drivers/driver-memory/src/memory-driver.ts:58 declares its vocabulary as [...FILTER_OPERATORS, '$like', '$ilike'], and the arms are at memory-driver.ts:1522 and memory-matcher.ts:458.
  • filter-refusal.ts is not an operator refusal here. The hasDanglingLikeEscape import at :36 gates a MALFORMED PATTERN — a lone unpaired trailing backslash — at :720-726, using the spec's shared test so every face refuses the same patterns. It does not refuse the operator.

The prose now states which faces answer, which refuse, and the dangling-escape rule that binds all of them. Paid in-file: two Common-Mistakes blocks in rules/filters.md that restated the Logical Operators and $null sections directly above them — the deletion that then required the baseline ratchet above.

Per-item ledger

id落点 (file:span at base 96b627d)beforeafter
QRY-A-01SKILL.md:3-9 frontmatter description"…picking pagination strategy, or designing a list view's filter spec" — but the real view filter spec is [{ field, operator, value }] over the 20-operator VIEW_FILTER_OPERATORS enum, which the package never namesthe list-view clause moves to the "Do not use for" arm and names objectstack-ui; a 3-row "which filter dialect" table routes ObjectQL $ / view rule / dataset measure filter
QRY-B-01SKILL.md:214-231, 274-295, 303-310, 316-338, 361-378, 380-399, 548-566seven rules stated twice, with cosmetically different examples, forcing the reader to pick an authoritydeleted from SKILL.md; rules/* are the anchor and keep one-line pointers. The rules files did NOT grow to absorb them — they already carried every one
QRY-B-02SKILL.md:43-51"When to Use This Skill", six bullets restating the description that already routed the loaddeleted
QRY-B-03SKILL.md:32-40"Skill Boundaries", a 4-row table restating the description and skills/README.md's cross-skill routingdeleted
QRY-B-04SKILL.md:20-52308 tokens of preamble before the first rule; the single most decision-changing fact — object is the first argument — appeared nowherethe file opens with the calling-convention construct
QRY-B-05references/_index.md:14generator-owned; the fix is upstream in packages/spec/src/data/query.zod.tsfiled as #14441, untouched here
QRY-C-01SKILL.md:496-539the search-mirror prescription in full, duplicating objectstack-data/SKILL.md — including the heading, the "one prescription" sentence and the formula-field paragraph — while :535-537 already named data as the anchorthe rule plus the pointer: "mirror the related record's title into a stored field … the field, the write hooks and the lint wording are objectstack-data → Search Fields (searchableFields)". Anchor re-verified after the data flight landed on main: objectstack-data/SKILL.md:100
QRY-C-02rules/filters.md:221-258, :282-286KEPT per the flight ruling: this package is the anchor for date macros / filter tokens. Confirmed at merge: the delivered ui flight (54a1125) now reads "The vocabulary, both accepted spellings, the two resolvers and the near-miss list are owned by objectstack-query → rules/filters.md"
QRY-C-03SKILL.md:617-632"CRM Analytics Query Blueprint", five rows of dataset/widget surface objectstack-ui owns, whose own closing line conceded the routingone routing line: dashboards and reports are metadata, model them in objectstack-ui
QRY-C-04rules/filters.md:260-28021 lines restating a flow-node rule objectstack-automation already statesone pointer line; anchor re-verified on the merged tree. See "follow-up for objectstack-automation" below for the two things the anchor does not carry
QRY-C-05SKILL.md:624, rules/aggregation.md:199-207compareTo documented twice here and a third time in full in objectstack-uiboth copies deleted, pointer kept. Established at source first: compareTo is LIVE, not retired — packages/spec/src/ui/dashboard.zod.ts:438 declares compareTo: strictObject({…}); what was removed is the BARE-STRING form and compareTo.offset (dashboard.zod.ts:272, :290). Live ⇒ delete + pointer. Target re-verified on the merged tree: objectstack-ui/SKILL.md:1410
QRY-D-01SKILL.md:274-279, 327-337, 440-445, 550-558; rules/aggregation.md:16-27, 138-144, 156-166; rules/pagination.md:12-18, 218-222, 160-177roughly half of ~1,173 tokens of tombstone callout was "why it was removed" narration for keys retiredKey() types neverone 6-row "removed key → live replacement" table in SKILL.md; the per-key history deleted in all three files
QRY-D-02rules/aggregation.md:88-96the alias/push-down bug-history parenthesis, describing behaviour that no longer existsdeleted; the live rule ("Read the result under alias ?? field") stays
QRY-D-03SKILL.md:20-28opening blurb restating the description, plus a three-way callout legend whose warn arm is used exactly oncedeleted; the legend's one live sentence folded into that single callout
QRY-D-04SKILL.md:87-88"inspired by Prisma, Strapi, and MongoDB" — positioning prosedeleted
QRY-D-05 / H-02 / H-03evals/README.mdDEFERRED to #14296 item 2.evals/** untouched, 558 tok unchanged
QRY-E-01whole package58 ts fences, 0 check-skill-examples markersNOT DONE — not in the card's IMPLEMENT list, and the corrected examples are engine.* call shapes, which are not resolvable from @objectstack/spec alone. See the gap ledger
QRY-E-02 (falsehood 1)SKILL.md:407-425 + Rules list :427-434the canonical expand example projected fields: ['title','status'] while expanding assignee and project — dropping both FK columns, so the query resolves NO relations and reports no errorthe example keeps the FK columns, and the requirement is now the FIRST Rules bullet. Contract: packages/spec/src/data/query.zod.ts:509 ("The projection must RETAIN the foreign-key column"); pinned at packages/objectql/src/query-expression-conformance.test.ts:1575-1586; mechanism re-located at source — packages/objectql/src/engine.ts:8358-8359 (const val = record[fieldName]; if (val == null) continue;) then :8372 (if (uniqueIds.length === 0) continue;)
QRY-E-03SKILL.md:60-69, 256-266, 280-294, 341-352, 407-425, 584-593 vs :371-377, 390-396, 604-612two incompatible calling conventions, neither named; the most-copied shape ({ object: 'account', … } as an option bag) is refused by every engine methoda 5-row calling-convention table with the legal key set per method, quoted from packages/objectql/src/engine.ts:368-407find/findOne = the nine query keys PLUS the six driver passthrough keys (review round 1); ENGINE_AGGREGATE_OPTION_KEYS = context, where, groupBy, aggregations, having, timezone; ENGINE_COUNT_OPTION_KEYS = context, where, both without passthrough and deliberately so — plus where a bare { object, … } literal IS correct (findData's query, an expand value) and the top/filter alias fold. Rejection pinned at packages/objectql/src/engine-unknown-option.test.ts:124-135
QRY-E-04SKILL.md:341-352, rules/aggregation.md:43-54both group-by examples carried fields: (and orderBy:), neither of which is in ENGINE_AGGREGATE_OPTION_KEYS; rules/aggregation.md:57-59 called fields "a readability convention" without saying it is rejectedboth shown as engine.aggregate(obj, {…}) with legal keys only, and the rejection stated
QRY-F-01absentcontext — 34 uses in packages/objectql/src/*.test.ts query literals, 40 in examples/** + packages/apps/** — had zero coverage in any packagea section: the RLS / system-read escape hatch, { isSystem: true } (the real spelling at examples/app-showcase/src/security/bind-position-sets.ts:50 and two siblings), and the query-bag vs trailing-argument rule with "the trailing one wins" (packages/objectql/src/engine.ts:1865-1877)
QRY-F-02 / G-03SKILL.md:464-495, :479-504only the object form search: { query, fields } was taught — 5 real usages against 74 for the bare string, which packages/spec/src/data/data-engine.zod.ts:120-127 calls "the canonical Tier-1 contract"; and three spellings of the narrowing knob appeared with none introducedthe bare string plus sibling searchFields is the canonical example (the shape packages/qa/dogfood/test/showcase-search.dogfood.test.ts:33,41 pins); one line names all three spellings and which layer takes which; the object form demoted to one line
QRY-F-03SKILL.md:136-146, rules/filters.md:18-21four string operators listed, three omitted ($icontains, $like, $ilike), and the case rule stated nowhere while the canonical examples were exactly the case-sensitive trapsthree rows added to BOTH tables, with the rule quoted rather than paraphrased from packages/spec/src/data/filter.zod.ts:548-549, the ASCII-only folding domain, and — after review round 1 — the measured split of which faces answer $like/$ilike and which refuse them
QRY-F-04SKILL.md:314-400, rules/aggregation.mdMED confidence, not in the card's IMPLEMENT listNOT DONE. See the gap ledger
QRY-G-02ten sitesfour spellings of one version factone spelling survives the merges

Funded additions from the delivered objectstack-ui flight

All five rows PR #14338 listed as absent here were verified at source before porting, and all five are TRUE. Paid inside rules/filters.md, whose named payer is QRY-C-04's deletion.

ui rowverified atported
both {token} and ${token} spellings are acceptedpackages/spec/src/data/date-macros.zod.ts:203DATE_MACRO_WRAPPED_RE is /^\$?\{([a-zA-Z0-9_]+)\}$/; context-tokens.zod.ts:109CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_REyes
two more near-misses: {user_id}, {organization_id}packages/spec/src/data/context-tokens.zod.tsCONTEXT_TOKEN_SUGGESTIONS maps user_id → current_user_id and organization_id → current_org_idyes
a token embedded in a larger string is not substitutedpackages/core/src/utils/filter-tokens.ts:50 — "Values that merely CONTAIN braces are left untouched"yes, with the 'user-{current_user_id}' specimen
isDateMacroToken(tok) as the author-time checkpackages/spec/src/data/date-macros.zod.ts:166, re-exported by packages/spec/src/data/index.ts:82yes, with its sibling isContextToken and the "pass the token WITHOUT braces" caveat
the build rule's name and why it existspackages/lint/src/validate-filter-tokens.ts:65export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown'yes, with the failure mode: an unresolved token reaches SQL as a literal, matches nothing, and the widget renders 0

None withheld.

Per-file token delta

countTokens is ceil(utf8 bytes / 4) (scripts/check-skills-token-ratchet.mjs:456). Figures measured at head 49de20f; round 3 changed no skill file, so they are round 2's.

filebeforeround 1headΔ vs beforeceiling
skills/objectstack-query/SKILL.md5,4433,7843,915−1,5285,552
skills/objectstack-query/rules/aggregation.md2,2261,8461,846−3802,357
skills/objectstack-query/rules/filters.md2,1442,1002,136−82,149
skills/objectstack-query/rules/pagination.md1,3811,1581,158−2231,382
edited skill files, total11,1948,8889,055−2,139 (−19%)
skills/objectstack-query/evals/README.md5585580 (deferred)567
skills/objectstack-query/references/_index.md5185180 (generated)
whole package12,27010,131−2,139 (−17%)

Every edited skill file shrank against its pre-flight size. No ceiling raised, no file added, no file deleted, scripts/check-skills-token-ratchet.mjs untouched.

Gap against the ≈ −3,530 target, itemised rather than smoothed

causetokens
QRY-C-02 (−380 in the audit) was OVERRIDDEN to KEEP by the flight ruling — this package is the date-macro anchor. The −3,530 figure is −4,088 − (−558), which still carries that −380+380
QRY-F-04 (≈ −400, MED confidence — reframing the aggregation cluster around a "which aggregation spelling" routing table) is not in the card's IMPLEMENT list, so it was not done+400
the five funded ui rows above post-date the audit and are not in its −4,088≈ +250
review round 1: the fifteen-key row and the measured $like face split, net of what was paid in-file+167
residual: every MERGE-INTO leaves a pointer line the audit's raw-span estimates did not price, and the E-03 / F-01 / A-01 constructs landed slightly above their +120 / +90 / +70 budgets≈ +194

premise_false

None of the audit's findings had a false premise; every span was re-verified at BASE (git diff --stat a59f78d 96b627d1 -- skills/objectstack-query is empty, so the audit's line numbers were exact). Corrections to stated premises, none of which changed a decision:

  • premise_false: PM mechanism assumption 2 (half) — the QRY-A-01 description edit regenerates content/docs/ai/skills-reference.mdx ONLY. skills/README.md is byte-unchanged: it renders just the description's FIRST sentence, which A-01 did not touch. The generator writes both files and check:skill-docs verifies both; one of the two simply had no delta.
  • premise_false: PM mechanism assumption 3 — no skills/objectstack-query heading is registered exhaustive. The Leg 2 BINDINGS table holds no row in this package. Run before the first deletion and after the last: green every time. No deletion here could red Leg 2.
  • premise_false: the audit's line cite for falsehood 1 — the FK skip is at packages/objectql/src/engine.ts:8358-8359, not :8345-8347 (which is a comment block). The mechanism is exactly as described and is confirmed, with the second half at :8372.
  • premise_false: review round 1's two readings of the $like facesdriver-memory answers $like/$ilike rather than refusing them, and filter-refusal.ts's hasDanglingLikeEscape gate is about a malformed PATTERN, not the operator; the three refusing faces do carry an INVALID_FILTER / 400 envelope. Evidence in the Review round 1 section above.

Also measured while executing the C-05 ruling: compareTo is a LIVE surface, not a retired one, so the live branch applied (delete + pointer, not delete with no pointer).

follow-up for objectstack-automation

QRY-C-04 deleted 21 lines here on the ruling that the flow-filter rule's anchor is objectstack-automation/SKILL.md. That anchor carries the core rule (a dropped filter condition WIDENS the query, so routing it would let a delete_record empty the object while the run reported success). Two things it does NOT carry, both deleted here:

  1. Precedence on config.filter. The flow template engine runs first, but hands a recognised filter placeholder through untouched for the engine to expand; a flow variable named after a placeholder shadows it.
  2. The static-checkability split.objectstack validate FAILS on a {record.…} filter token naming an unknown field, or hopping through a relation the start node does not expand; the same reference OUTSIDE a filter (a message body, an http url, a write payload) stays a warning, because there it renders a blank rather than widening a query. An unresolved flow variable is not statically checkable at all.

Neither is a query-DSL rule, so neither belongs here. Suggested for that package's own flight.

No follow-up for objectstack-data (the anchor is complete, re-verified after that flight landed) or objectstack-ui: the audit's incidental falsehood 2 (the ui claim that unknown placeholders "silently pass through as literal strings", contradicted by packages/core/src/utils/filter-tokens.ts:378) is already gone from main — the ui flight landed as 54a1125 and now points at rules/filters.md, which states it correctly.

Gates

Head sha 49de20f, a merge of current origin/main (a98b61b) into the branch, so the derivation is not from a stale tree. Exit codes captured before any pipe.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands re-derived over the now 6-path change set: 42 commands — six more than rounds 1-2, pulled in by scripts/role-word-baseline.json: check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal. All 42 were run at 49de20f:

  • 41 green.
  • 1 NOT MEASURED, by that gate's own design: node scripts/check-test-completeness.mjs exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED. ⛔ It is not a red." Recorded as NOT MEASURED, not as green.

The gate CI caught, now green, plus the six newly-pulled families:

✓ check-role-word — Ledger: 43 baselined file(s) still carrying it (126 occurrence(s)) in scripts/role-word-baseline.json.
✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path
✓ check:entry-guard: 198 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 149 export bindings, 147 of them inert on import
✓ check:parse-guard (exit 0)
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 32 file(s) resolve against 78 workspace package(s)
✓ check-watch-hint-literal: 43 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

The five card-named gates, at the same head:

✓ check-skills-token-ratchet: 37 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
✅ 265 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
check-skill-identifier-liveness OK — Leg 1: 491 citation(s) over 47 published file(s) checked against 92934 implementation word tokens (3 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
✅ Skill docs in sync
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 78 workspace packages

(The ratchet's file count and Leg 2's binding count both dropped by one against round 2 because the data flight landed on main between the rounds and deleted skills/objectstack-data/rules/hooks.md; nothing in this PR moved either number.)

check:skill-examples ran against a freshly rebuilt @objectstack/spec + @objectstack/client-react closure (pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/client-react...' build, exit 0), so it refused nothing rather than false-greening on a stale dist. Heavy steps ran under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14302.

Why round 2 shipped a red: round 2 re-ran only the five card-named gates instead of re-deriving the union, and check:role-word is reached by the prose paths rather than by the card's list. Round 3 runs the whole re-derived list, which the new baseline file itself widens.

skip-changeset

Nothing released by any package changes. The change set is 4 files under skills/, one generated file under content/, and one gate baseline under scripts/; scripts/check-empty-changeset.mjs:357-360 enumerates exactly that population:

* It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like)-> delete the changeset and apply the 'skip-changeset' label (route 2). The label is a gate-level exemption: it produces NO input for changesets/action.

Route 2 taken: no changeset file, label applied.

Out of scope, filed

No second out-of-scope card is owed from review round 1: the spec .describe() at packages/spec/src/data/filter.zod.ts:697 was checked against all three faces and is accurate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

QRY-D-01 — delete the per-key "why it was removed" narration from
rules/aggregation.md (`distinct: true`, `array_agg`/`string_agg`,
`windowFunctions`) and rules/pagination.md (`cursor`, `distinct`, and the
`cursor` Common-Mistakes pair). The keys are tombstoned: `tsc` types them
`never` and a query carrying one fails to parse with the prescription, so the
only decision-changing half is the replacement — now one table in SKILL.md.
QRY-D-02 — delete the alias/push-down bug-history parenthesis at
rules/aggregation.md; the behaviour it describes no longer exists.
QRY-E-04 — the group-by example is shown in the real `engine.aggregate(obj, …)`
shape with legal keys only; `fields` is not in ENGINE_AGGREGATE_OPTION_KEYS and
is rejected by name, which the old "readability convention" note did not say.
QRY-C-05 — `compareTo` is objectstack-ui's surface; the copy here becomes a
pointer.
aggregation.md 2226 -> 1846 tok, pagination.md 1381 -> 1158 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-C-04 — the "a flow filter that loses a condition refuses to run" rule is a
flow-node authoring rule, and objectstack-automation already states it. Deleted
here; one pointer line stays. (Two things the automation anchor does NOT carry
are listed in the PR body as a follow-up for that package.)
QRY-F-03 — the string-operator table listed four operators and omitted three
that exist: `$icontains`, `$like`, `$ilike`. Added, together with the case rule
`packages/spec/src/data/filter.zod.ts` states outright ("`$contains` /
`$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY.
`$icontains` is the case-INSENSITIVE twin"), the ASCII-only folding domain, and
the faces that refuse `$like`/`$ilike` rather than approximating them.
Funded additions from the delivered objectstack-ui flight, each verified at
source before porting — this package is the anchor for the token vocabulary and
ui now points here: both `{token}` and `${token}` parse (DATE_MACRO_WRAPPED_RE,
shared by the context tokens); `{user_id}` and `{organization_id}` join the
near-miss list (CONTEXT_TOKEN_SUGGESTIONS); a token embedded in a larger string
is left untouched; `isDateMacroToken` / `isContextToken` from
`@objectstack/spec/data` are the author-time check; the build rule is named
(`filter-token-unknown`) along with why it exists.
Paid in-file by C-04 plus two schema-enforced Common-Mistakes blocks.
2144 -> 2100 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
QRY-E-03 / QRY-B-04 — the package used two incompatible calling conventions and
named neither, and its most-copied example shape (`{ object: 'account', … }`
passed as an option bag) is not accepted by any engine method:
ENGINE_FIND_OPTION_KEYS and ENGINE_AGGREGATE_OPTION_KEYS are closed sets and an
unlisted key is refused by name. The file now opens with the convention —
object is the FIRST ARGUMENT, the legal key set per method, and where a bare
`{ object, … }` literal IS correct (findData's `query`, an `expand` value).
QRY-B-01 — the seven rules stated twice are merged into rules/*: field
references, keyset pagination, the OData alias, the aggregation functions
table, HAVING, filtered aggregation and window functions. SKILL.md keeps
one-line pointers.
QRY-D-01 — the four removal-history spans become one 6-row "removed key ->
live replacement" table.
QRY-C-01 — the search-mirror prescription is objectstack-data's (this file
already named data as the anchor); reduced to the rule plus that pointer.
QRY-C-03 / QRY-C-05 — the CRM Analytics Query Blueprint and `compareTo` are
objectstack-ui's dataset/widget surface; one routing line replaces them.
QRY-B-02 / QRY-B-03 / QRY-D-03 / QRY-D-04 — "When to Use This Skill", "Skill
Boundaries", the opening blurb, the callout legend and the positioning prose
restate the frontmatter or the catalog and change no decision.
QRY-E-02 (incidental falsehood 1) — the canonical `expand` example expanded
nothing: it projected `fields: ['title','status']` while expanding `assignee`
and `project`, dropping both foreign-key columns, and the engine skips a
relation whose FK is absent. The example keeps the FK columns and the
requirement is now the first Rules bullet.
QRY-F-01 — `context` had zero coverage; it is the RLS / system-read escape
hatch, and a read without it silently returns fewer rows. Added with the
`{ isSystem: true }` example and the query-bag vs trailing-argument rule.
QRY-F-02 / QRY-G-03 — the bare-string `search` plus sibling `searchFields` is
the canonical Tier-1 form; the three spellings of that one knob are named.
QRY-F-03 / QRY-A-01 / QRY-G-02 — the three missing string operators and the
case rule; a "which filter dialect" table; one spelling for the version fact.
QRY-A-01 also edits the frontmatter description: the list-view filter spec is
objectstack-ui's vocabulary, not this package's.
5443 -> 3784 tok.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…tion edit
Generator output only — `pnpm --filter @objectstack/spec gen:skill-docs`.
`skills/README.md` is byte-unchanged: it renders only the description's FIRST
sentence, which QRY-A-01 did not touch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… $like faces
Two spans, both re-measured at source before touching them.
1. The calling-convention table listed nine keys for engine `find`/`findOne` and
called the set closed. `packages/objectql/src/engine.ts:396-400` spreads
`ENGINE_DRIVER_PASSTHROUGH_KEYS` (`:368-370`: `transaction`, `tenantId`,
`tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) into it, so as
written the table said an explicit `tenantId` is refused, which is false. The
row now names all fifteen, and the prose carries the reason the source gives
at `:360-366`: on find/findOne (and update/delete) the option bag IS the base
of the driver options, while `count`/`aggregate` never forward the bag, so
those keys are deliberately illegal there. `count` and `aggregate` rows
unchanged — both were already exact.
2. Review asked me to verify the `$like`/`$ilike` face list at source or drop
it. Verified; all three refusals hold, and each is an ALLOWLIST MISS, which
is why grepping the drivers for `$like` does not find them:
- driver-mongodb — `mongodb-filter.ts:1071` throws `Unsupported filter
operator "$like" …`; its `unsupportedFilterError` (`:366-371`) sets
`INVALID_FILTER` / 400. Pinned at
`mongodb-operator-key-clobber.test.ts:158-161`.
- objectql `having` — `having-filter.ts:136-141` `CONDITION_OPERATORS` is
sixteen operators without `$like`/`$ilike`; anything else reaches
`unknownOperator` (`:174-205`) and `invalidFilterError`, `INVALID_FILTER`
/ 400 (the table at `:157-158` names the faces sharing that envelope).
- service-analytics — `strategies/filter-normalizer.ts:418-435`
`MONGO_TO_CUBE_OP` omits them; the miss branch at `:1032-1046` throws
`invalidFilterError` (`:403-406`, `INVALID_FILTER` / 400) with
`Unsupported filter operator "$like" …`.
The review's own two readings were the ones that did not survive measurement,
so the sentence is corrected rather than weakened: `driver-memory` ANSWERS
these operators (`memory-driver.ts:58,1522`, `memory-matcher.ts:458`), and
`filter-refusal.ts:720-726` refuses only a DANGLING ESCAPE in the pattern via
the spec's shared `hasDanglingLikeEscape`. The prose now states which faces
answer, which refuse, and the escape rule that binds all of them.
Paid in-file: `rules/filters.md` 2100 -> 2136 (ceiling 2149, and still under the
2144 it started at) by collapsing two Common-Mistakes blocks that restated the
Logical Operators and `$null` sections above them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant changed the title skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,306 tokens)skills(query): optimization flight — seven twice-stated rules merged into rules/, removed-key history collapsed to one table, search/flow-filter/compareTo pointed at their anchors, context and the calling convention taught (net −2,139 tokens)Sep 2, 2026
`check:role-word` reds on IMPROVEMENT as well as regression: round 2's deletion
of two Common-Mistakes blocks in `skills/objectstack-query/rules/filters.md`
removed one baselined occurrence, so the gate reported
role-word count improved 9 -> 8 — ratchet DOWN: run
`node scripts/check-role-word.mjs --update` and commit the baseline.
This is that commit, and nothing else: `node scripts/check-role-word.mjs
--update` moved exactly one row (9 -> 8) and no other, verified by `git diff`
before committing. The gate is green after it.
Why it reached CI rather than my machine: round 2 re-ran only the five
card-named gates instead of re-deriving the union, and this family is pulled in
by the prose paths, not by the card's list. Round 3 runs the whole re-derived
list — which this baseline file itself widens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queueSep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33625162151 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL unit test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
    ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 04ec174Sep 2, 2026
39 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude