Repository navigation
feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search - #20487
Conversation
…uped answers under a search group the searched rows EngineAggregateOptionsSchema declares search / searchFields exactly as EngineQueryOptionsSchema does; ObjectQL.aggregate accepts them and runs the one ADR-0061 expander find runs; findData's grouped branch passes them through. Pinned at the public door (POST /data/:object/query) on both aggregate tiers. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…, every declared key executed Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ed a second shape with search (ADR-0122) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…eference for the aggregate search keys Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…gregate-honours-search
… limit Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…cases instead of erasing them Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…gregate-honours-search # Conflicts: # packages/spec/src/type-alias-convention.pin.test.ts
…igins from the merged tree The os-regen driver kept the branch's side of all three on the merge of origin/main b810ddb, dropping main's rows (the empty-operator exports, among others). Regenerated after a fresh spec build; the result differs from origin/main by exactly this PR's rows: EngineAggregateOptionsParsed (api-surface, export-origins) and EngineAggregateOptions:search / :searchFields (authorable-surface). Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e420e174128269e203658b042d036956ea1a14b6 && git checkout e420e174128269e203658b042d036956ea1a14b6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9801da1234761c14eb88663635792723e41d359f 91e08b5f6402f55bd107c2cbc8e5e34fa74aa817 && git checkout -B drift-repro 9801da1234761c14eb88663635792723e41d359f && git merge --no-ff 91e08b5f6402f55bd107c2cbc8e5e34fa74aa817
node scripts/docs-audit/affected-docs.mjs --json 9801da1234761c14eb88663635792723e41d359f
|
Contract reviewServed-tier: Inputs, and nothing else: card #20358 (body and every comment — the filing and triage Check-runs on this head: none exist. The check-runs listing for ① Derived judgmentsAccept-set changes.
Public-surface changes.
Security ordering, answered from the code on both verbs.
One expander, both tiers.
Protocol.
Tests and the sibling. The rest door §10 (11 cases, both tiers with the serving tier asserted, flat-parity and unsearched controls, ② Semver level
③ Boundary flagsDev deviations (
Additional flags raised by this review, none blocking:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…its that decided them (stage 3) (objectstack-ai#20533) Part of objectstack-ai#20234 Clause-②: no ## What changed This is stage 3 of the staged sweep. It covers `packages/spec/src/data/**` and nothing else. It leaves out the files an open PR or an in-flight claim holds: `data-engine.zod.ts`, `data-engine.test.ts`, `hook.form.ts`, `analytics*.ts`, `cube-member-inner-name-retirement.test.ts`, `driver/turso.zod.ts` and `filter-subtree-provenance.ts`, as the claim names them. It also leaves out four files that open PRs started editing after the claim: `driver/turso.test.ts` (PR objectstack-ai#20504, objectstack-ai#20437's, opened 2026-09-28T20:08Z), `object.form.ts` (PR objectstack-ai#20519, objectstack-ai#20432's, 21:55Z), `object.zod.ts` (PR objectstack-ai#20521, objectstack-ai#20494's, 22:10Z) and `filter-logic-conformance.ts` (PR objectstack-ai#20523, objectstack-ai#20444's, 22:39Z). See Acceptance notes. Later stages cover the other areas, so this PR says `Part of`. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123). That is **163 sites on 161 lines in 44 files, covering 40 numbers**. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and it says in its own words what that commit decided. No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of the 43 dead numbers in scope. ADR-0104 names objectstack-ai#12380 only as a reference, and ADR-0055 states the rule that objectstack-ai#8772's ruling enforced, not the ruling itself. So every anchor is a commit: **38 distinct shas**. One number was dropped rather than anchored: objectstack-ai#17286, a tracking card that recorded an axis as undecided, under which no commit landed. The sentence keeps its reason in words. Three comment sites in scope are left on purpose (see Acceptance notes). Two are the `[objectstack-ai#6259]` marker in `api-derivation.ts:163`, which a test string reads, and the test comment that names that marker. The third is `field.zod.ts:370`, whose `objectstack-ai#6111` is objectui's number. Only comments changed. Every source file keeps its line count (174 lines out, 174 in, over 45 files), so no line citation into these files moves. Thirteen of those 174 lines held no dead citation. Eleven are the other half of a sentence that had to be reflowed or rewritten. One is a table header (`value-roundtrip-conformance.ts:20`, 「card」 to 「card or commit」, because its row now holds a commit). One is `api-derivation.ts:164`, which now carries the `[objectstack-ai#6259]` sentence's commit. No code token moves (see the guard below). The 41 string-literal sites that carry a dead number are tokens, so they are left as they were and listed below. **No citation number is added.** Every tracker number on an added line was already on the line it replaces. No PR number stands on an added line. Two more kinds of file change, both mechanical: - **One regenerated reference page.** Two of the rewritten docblock lines (`feed.zod.ts:15`, `:18`) project into `content/docs/references/data/feed.mdx`. `check:docs` proved that page stale, and `pnpm --filter @objectstack/spec check:generated --fix` regenerated only it. The diff is two lines, each the same substitution as its source line. No page a held file projects into (`analytics.mdx`, `data-engine.mdx`, `hook.mdx`, `driver-turso.mdx`) moved. - **A `patch` changeset** for `@objectstack/spec` (see Changeset below). ## Census: `data/`, before and after **Instrument.** This is the instrument of stages 1 and 2. It sends REST `GET /repos/objectstack-ai/objectstack/issues/N` without following redirects, for every distinct number cited in `packages/spec/src/data`. The population is: - the citation gate's own exported `CITATION_RE` and `NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`, `objectstack-ai/objectstack`, `framework`, `pre-` or `post-`; - widened here to the capitalised spellings of those qualifiers (`Pre-`, `POST-`, `Framework`: 7 sites, one of them dead), which stage 2's case-sensitive set did not read; - N of 100 or more, excluding `summon` heads. Each site is classified by the TypeScript parser as a line comment, a docblock, a block comment or a string. **Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at the start, after every 100 numbers and at the end. They read 24 of 24 lit (200) and 24 of 24 dead (404) over 8 checkpoints in both runs. | reading | tree | numbers probed | 200 | 404 | 301 or other | dead sites, all of `data/` | in scope | excluded (held files) | in-scope lines | in-scope files | dead numbers in scope | |---|---|---|---|---|---|---|---|---|---|---|---| | before | base `9bf5e67af`, probed 2026-09-28T19:32Z to 19:36Z | 618 | 571 | 47 | 0 | **240** | 207 | 33 | 204 | 47 | 43 | | after | head `96fd49caa2`, probed 2026-09-28T23:19Z to 23:23Z | 600 | 571 | 29 | 0 | **77** | 44 | 33 | 43 | 16 | 21 | **Before, in scope, by class.** 92 non-test docblock sites and 13 non-test line comments. 16 test docblock sites and 45 test line comments. 39 test string sites. 2 non-test string sites. **After, in scope.** 41 string sites and 3 comment sites remain, all three deliberate. The head probe found no number newly dead since the base probe: the same 571 numbers answer 200. PR objectstack-ai#20226's area table read `data` 239 at an earlier base; this census reads 240 at `9bf5e67af`. The 33 excluded sites sit in `object.zod.ts` (15), `analytics.zod.ts` (3), `analytics-strictness-batchd.test.ts` (2), `analytics-date-range-two-bound-window.test.ts` (1), `driver/turso.zod.ts` (2), `driver/turso.test.ts` (3), `filter-subtree-provenance.ts` (3), `filter-logic-conformance.ts` (3) and `object.form.ts` (1). `data-engine.*` and `hook.form.ts` carry none. ## Per-number table The counts are in-scope sites and files at the base. `rewritten / left` gives comment sites rewritten and sites left. Every anchor was read in its diff or message, not only in its subject: it is the commit that made the change the line now describes, and its own diff or message names the number it replaces. | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#6111` (objectui) | 1/1 | 0/1 | objectui's number, left: see Acceptance notes | | `objectstack-ai#6259` | 5/2 | 1/4 | `6968885ef`: retires the producer-less `batch: 'bulk'` row of `DATA_ACTION_TO_API_OPERATION` and the prose calling `batch` a runtime action. The marker and 2 test strings stay (see Acceptance notes) | | `objectstack-ai#6345` | 18/5 | 17/1 | `e2798fab7`: one driver vocabulary; both boot hosts read the shared table; `mongo` to `mongodb`; turso a builtin; the fork-1 and fork-2 refusals | | `objectstack-ai#6571` | 10/2 | 8/2 | `2f3e79351`: `$between` endpoints accept the ISO/clock strings the platform produces, as a bare string (rider ①) | | `objectstack-ai#8495` | 9/2 | 6/3 | `4bfe1a539`: refuses `${…}` placeholders in memory `persistence.path` / `persistence.key` at publish | | `objectstack-ai#8656` | 1/1 | 0/1 | a test title only | | `objectstack-ai#8696` | 20/8 | 17/3 | `90a12fb18`, the card's mongodb arm: a bound secret rides beside an unmodified url as MongoClient `auth`. Its own pins carry the multi-host form `new URL()` cannot parse and the bound secret outranking `options.auth` | | `objectstack-ai#8772` | 3/2 | 3/0 | `75b7c240a`: Direction 2 of the 2026-08-16 maintainer ruling. The builder forces `required: true` on a `master_detail` under `controlled_by_parent`, and raw parse stays tolerant. ADR-0055 stays cited beside it | | `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2`: stamp-only `tenancy.organizationField`, declared by `sys_api_key` | | `objectstack-ai#8794` | 2/1 | 2/0 | `1850ebbb0`: corrects the reuse-safety claim on the filter-subtree mark from the survey's measurement, and routes a mechanism change to a spec-seat ruling (stage 1's anchor too) | | `objectstack-ai#8836` | 2/1 | 2/0 | `1850ebbb0`: the same commit, which pins the invariant (one line carries both numbers) | | `objectstack-ai#8873` | 6/3 | 6/0 | `096106522`: a bound `credentialsRef` reaches the postgres server on the DSN branch. Its diff records that `pg` sends a password only when the server asks | | `objectstack-ai#8874` | 1/1 | 1/0 | `d70428ae7`: a declared mysql `ssl` reaches `mysql2` as its own TLS options object, because `mysql2` rejects a bare boolean | | `objectstack-ai#8876` | 9/5 | 6/3 | `d634e665b`: exports `urlUserinfoUsername`, and its diff states the asymmetry that a username is not credential material | | `objectstack-ai#9040` | 20/6 | 14/6 | `24206416a`: refuses a credential in the mongo options passthrough at publish, and redacts the passthrough secret paths on read | | `objectstack-ai#9041` | 22/2 | 17/5 | `d491625c1`: refuses a bound `credentialsRef` with a user-less mongo `config.url`, with the triage's fences | | `objectstack-ai#10165` | 5/1 | 1/4 | `801296050`: `ttl.onlyWhen` with the canonical null predicate (maintainer ruling 2026-08-20, option A) | | `objectstack-ai#10274` | 1/1 | 1/0 | `d1ba685ec`: re-measures the objectui pin citations and gates the class | | `objectstack-ai#10329` | 6/2 | 6/0 | `15d58dbf1`: retires the import lookup transform's steering params (ADR-0049) | | `objectstack-ai#10347` | 2/1 | 2/0 | `530c1df65`: the Archiver honours a declared `ttl` (maintainer ruling 2026-08-20) | | `objectstack-ai#10527` | 2/1 | 1/1 | `5649efbf9`: refuses a diverging retention + ttl + archive triple at parse time | | `objectstack-ai#11065` | 7/3 | 5/2 | `20950404c`: a boolean aggregand counts as 1 or 0 in `avg` and `sum`, the first face aligned. No commit message names the card; this is where the number first entered the tree | | `objectstack-ai#11195` | 3/1 | 2/1 | `b37231883`: `UserActionsConfigSchema` adopts `group` / `hideFields` / `rowColor` | | `objectstack-ai#11215` | 1/1 | 1/0 | `42a117b88`: documents `NoSQLIndexSchema.unique`'s deliberate scope-vocabulary omission | | `objectstack-ai#11350` | 1/1 | 1/0 | `ece4dad31`: records the 2026-08-23 maintainer ruling on entry nameability (stage 1's anchor too) | | `objectstack-ai#11408` | 2/1 | 1/1 | `f11fc61c5`: declares `editMode` (maintainer ruling 2026-08-24) | | `objectstack-ai#11507` | 5/2 | 5/0 | `88b9d749a`: declares `sys_activity.type` an open, author-extensible vocabulary (maintainer ruling 2026-08-24, direction 4) | | `objectstack-ai#11658` | 1/1 | 1/0 | `1a6a19c31`: opens `RecordActivityProps.types` to author-contributed kinds | | `objectstack-ai#12380` | 4/2 | 4/0 | `4045b954d`: makes the SQLite `Field.json` codec injective; its message carries the measured boundary | | `objectstack-ai#12868` | 1/1 | 0/1 | a test title only. Its comment site sits in `object.form.ts`, now held by PR objectstack-ai#20519; its deciding commit is `c459da6bc` (see Acceptance notes) | | `objectstack-ai#13156` | 1/1 | 1/0 | `fd289be45`: strips tracker ids from function-declaration-built refusal prose (the card's A half) | | `objectstack-ai#13644` | 3/2 | 2/1 | `34ce8e7db`: declares `ctx.referentialFieldClear` on `HookContextSchema` | | `objectstack-ai#14426` | 2/2 | 1/1 | `40a44b91b`: the undefined-comparand refusal prescribes the null predicate by its ruled spellings, position-safe | | `objectstack-ai#14676` | 1/1 | 1/0 | `13c48c2a5`: retires `connector.errorMapping`; its test states the same assertion-set reasoning | | `objectstack-ai#16126` | 2/2 | 2/0 | `859ded3ec`: refuses a whitespace-only `reference` on lookup / master_detail | | `objectstack-ai#16685` | 4/2 | 4/0 | `ed7243d52`: accepts boolean / toggle for sum / avg / min / max (decision batch objectstack-ai#80) | | `objectstack-ai#16867` | 3/2 | 2/1 | `0ee32edef`: `notNull` / `not_null` prescribe `storage.notNull`, not `required` | | `objectstack-ai#17014` | 3/2 | 2/1 | `80aef8032`: the one-day date-range presets prescribe a one-day window, and the table states its end-token convention | | `objectstack-ai#17286` | 1/1 | 1/0 | dropped: a tracking card with no landing. The sentence now says the card is gone and to measure `driver-memory` for the open set | | `objectstack-ai#17348` | 1/1 | 1/0 | `51efbf116`: pins the `driver-memory` temporal text-operator divergence by name in that driver's conformance suite | | `objectstack-ai#17590` | 1/1 | 1/0 | `e04a0aff2`: `$contains` on a JSON column is a per-dialect membership test (director-seat ruling 2026-09-12) | | `objectstack-ai#18012` | 8/3 | 7/1 | `176b03582`: `$between` requires two non-blank endpoints (decision batch objectstack-ai#146 item 5, letter A) | | `objectstack-ai#19377` | 6/2 | 6/0 | `a60c913de`: refuses a `{ $field }` reference as a `$between` endpoint at the runtime filter door | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0). That is 38 distinct shas. Wordings to check, each true of its commit: - `datasource.zod.ts:352` names only the card's mongo arm (`90a12fb18`) for "the defect class … closed", because the paragraph is about mongo. The card's mysql arm (`72050cc47`) is not cited anywhere in this stage. - `datasource.zod.ts:354`: 「the triage's, as commit d491625 landed them」. `d491625c1`'s message lists the fences as "per triage". - `filter.zod.ts:1021-1025`: the `objectstack-ai#17286` pointer becomes 「was measured on a tracking card … That card is gone: measure `driver-memory` for the open set, ⛔ not this text.」 The warning that this paragraph is not the authority is kept. ## The 41 string sites left as tokens - **Test titles and test-code strings (39 sites).** `driver/driver-credential-refusal.test.ts` 14, `object.test.ts` 6, `datasource-credential-redaction.test.ts` 3, `driver/driver-placeholder-refusal.test.ts` 3, `filter.test.ts` 3, `api-derivation.test.ts` 2 (the `split('[objectstack-ai#6259]')` literal and its message), `field.test.ts` 2, and 1 each in `date-range-presets.test.ts`, `driver/postgres.test.ts`, `field-rows-option-description.test.ts`, `filter-comparand-type.test.ts`, `hook.test.ts` and `object-strictness-batch20.test.ts`. - **Non-test strings (2 sites).** `aggregation-conformance.ts:398` and `:407`, the `note` of two exported `AGGREGATION_CASES` rows (`objectstack-ai#11065`, `objectstack-ai#11151`). They ship as data. Their only readers are driver conformance suites, which print a `note` as the assertion message when a case fails, to a driver developer and never to a metadata author. So they are neither comments nor form D author-shown text. This is the same disposition stage 1 gave the two `why` strings and stage 2 the `PROVENANCE_WAIVERS` reason. No author-shown text in `data/` carries a dead number, so nothing here is objectstack-ai#20233's form D. ## Mechanical guard: no code token moves The check compares leaf tokens with comments stripped, base `9bf5e67af` against head `96fd49caa2`. It uses the TypeScript parser's leaf tokens, so template literals are scanned in context, and it excludes JSDoc nodes. It ran over all 45 touched `.ts` files. - Real run: 140,379 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control: 0 files changed, as expected (exit 0). - Positive control (a declaration inserted into `feed.zod.ts`): 1 file reads DIFFER (exit 1). - Positive control (one digit changed inside the `split('[objectstack-ai#6259]')` string in `api-derivation.test.ts`): 1 file reads DIFFER (exit 1). ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/spec` is included. It says only that the provenance comments were re-anchored. Measured on the built package: 14 of the touched sources are `src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten docblocks also reach `dist`. `88b9d749a`, `e2798fab7` and `24206416a` each appear in 1 declaration file. `24206416a` appears in 20 bundled `.js` files and `2f3e79351` in 28. The positive control, a pre-existing `feed.zod.ts` docblock sentence, appears in `dist/data/index.d.ts`. ## Gates (head `96fd49caa2`) - **Citation judging pass, run as CI runs it:** `pnpm check:issue-citations && node scripts/check-issue-citations.mjs` exits 0. The self-test passes 73 cases in 7 batteries. The live run judged 11 citations across 25 files, and all 11 resolve. - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at the final head derived 108 families, and all 108 exit 0. `--ran` reports 108 run, 0 NOT MEASURED, 0 unrun, and exits 0. (`check:i18n` was derived at the earlier heads from `object.form.ts`, and left the set when that file went back to base.) - At an earlier head, four gates first exited 3 (PREREQUISITE NOT MET) because the workspace was unbuilt: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples` and `check:docs-transcript-drift`. At the final head a full `turbo run build` of `./packages/*` ran first (71 tasks, exit 0, under the shared verify lock), and every gate exited 0 on its first run. - `check:generated` was run under the lock against that build: all 15 artifacts are up to date. - **Build, tests, typecheck and lint:** - `pnpm --filter @objectstack/spec build` exits 0. - `vitest run --maxWorkers=2 src/data` in `packages/spec` at the final head: 107 files and 3,517 tests pass (1 todo), covering every touched test file. - The 12 spec suites outside `src/data` that read `data/` source text pass at the final head: 12 files, 503 tests. These are `scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`, `src/api/api-entry-graph.pin.test.ts`, `src/contracts/scoped-context.test.ts`, `src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`, `src/system/constants/platform-object-names.test.ts`, `src/type-alias-convention.pin.test.ts` and `src/ui/dashboard.test.ts`. - `pnpm --filter @objectstack/spec typecheck` at the final head exits 0, including `check:test-typecheck` (53 files, 251 errors, 138 pinned signatures held). - Lint, as a proven narrowing at the final head: `eslint --no-inline-config --format json` over the 45 touched `.ts` files gives 45 files, 0 errors and 0 warnings. All 45 are in eslint's own population (`isPathIgnored` is false for each). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, which its own line 328 states), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. ## Acceptance notes - **The `[objectstack-ai#6259]` marker.** `api-derivation.test.ts:236` splits `DATA_ACTION_TO_API_OPERATION`'s TSDoc on the literal `[objectstack-ai#6259]`, and a test string may not change here. So the marker line `api-derivation.ts:163` is byte-identical to the base, and the test comment at `:232` that names the marker stays too. The sentence's deciding commit sits on the next line instead: 「(both by commit 6968885)」. A first attempt wrote the commit onto the marker line itself. The diff-scoped `check-issue-citations` then read the kept `objectstack-ai#6259` as an added citation and exited 1, so it was moved one line down (commit `b93f08f8d0`). - **objectui's `objectstack-ai#6111`.** `field.zod.ts:370` reads 「objectui#6110 + objectstack-ai#6111 (section)」. The qualifier covers only the first number, so the citation grammar reads `objectstack-ai#6111` as this repository's (404 here). It is objectui's number: its introducing commit `f887e5249` writes `(objectui#6111)` in the same diff, and `objectstack-ai/objectui` answers REST 200 for objectstack-ai#6111 to this session (and for objectstack-ai#6110 and objectstack-ai#10264). objectui has no `refs/pull/6111/head`, so it is an issue there, not a PR. The line is left unchanged. This is objectstack-ai#20330's grammar family, the same as stage 2's `objectui PR objectstack-ai#10264`, and it is noted there, not filed. - **Capitalised qualifiers.** `CITATION_RE` classes `Pre-#N`, `POST-#N` and `Framework#N` (7 sites in `data/`) as cross-repo and never judges them. This census read them as this repository's. One was dead and is rewritten here (`object.test.ts:223`, `POST-objectstack-ai#10347`). This is the same objectstack-ai#20330 family as stage 1's `pre-` / `post-` finding. - **Four files held after the claim.** Each joined the exclusions and went back to the base bytes (hypothesis 2 of the dispatch). Each PR's hunks were disjoint from this PR's lines, but the dispatch's rule is file-level. - `driver/turso.test.ts`: PR objectstack-ai#20504 (objectstack-ai#20437's) opened at 2026-09-28T20:08Z and edits it. Its two comment sites (`:4`, `:58`, both `objectstack-ai#6345`) went back to blob `7fe99ebf9` in commit `86463ed0a1`. A no-driver `merge-tree` of that head with PR objectstack-ai#20504's head `5dfa45e9f` exits 0. - `object.form.ts`: PR objectstack-ai#20519 (objectstack-ai#20432's) opened at 21:55Z and edits it. Its one comment site (`:256`, `objectstack-ai#12868`, whose deciding commit is `c459da6bc`) went back to blob `60713e06f` in commit `3479600dda`. - `object.zod.ts`: PR objectstack-ai#20521 (objectstack-ai#20494's) opened at 22:10Z and edits one line at `:2123`. Its 15 comment sites (`objectstack-ai#8772`, `objectstack-ai#10165`, `objectstack-ai#10347`, `objectstack-ai#10527`, `objectstack-ai#11195`, `objectstack-ai#11408`, `objectstack-ai#13608`) went back to blob `befde04ca` in commit `96fd49caa2`. Their deciding commits are `75b7c240a`, `801296050`, `530c1df65`, `5649efbf9`, `b37231883`, `f11fc61c5` and `fc9ba76a5`, all read for this stage. - `filter-logic-conformance.ts`: PR objectstack-ai#20523 (objectstack-ai#20444's) opened at 22:39Z. Its 3 comment sites (`objectstack-ai#13195`) went back to blob `c9b32acba` in the same commit. Their deciding commit is `9dac1ae01`, with `PR objectstack-ai#13529` as the link. - **What stays for later stages.** - The 33 dead sites in the held files listed above. The later stage can reuse the deciding commits named for them here. - The 41 string sites and the 3 deliberate comment sites above. - The `data/` numbers that also appear in `packages/spec/src/migrations/**`. Those are objectstack-ai#20233's form D, or the migrations stage. - **The rung.** Several anchored changes also have ADR-0087 entries in `packages/spec/src/migrations`. Examples are `cbp-master-detail-required-forced` for objectstack-ai#8772, `filter-between-blank-endpoint-refused` for objectstack-ai#18012, the `datasource-*` entries for objectstack-ai#9040, objectstack-ai#9041 and objectstack-ai#8873, and the `mapping-lookup-params-removed` conversion for objectstack-ai#10329. This PR takes the commit rung, as stages 1 and 2 did, so it is precedent-consistent. The D3 id is the more durable in-repo record, if the ruling's first rung is later read to include those entries. - **The citation gate's reach.** It defers `packages/**/*.test.ts`, so 20 of the 45 touched `.ts` files never enter its judging population. The added-minus-removed count over the whole diff covers them: 0 numbers added. - **Base.** The branch is 22 commits behind `origin/main` (`1378ec7c0c`, read at 2026-09-29T00:18Z). Four of those commits touch `data/`, all in excluded files: objectstack-ai#20475's `hook.form.ts`, objectstack-ai#20487's `data-engine.*`, and, since this stage excluded them, PR objectstack-ai#20521's `object.zod.ts` (`9e1689f8e2`) and objectstack-ai#20444's `filter-logic-conformance.ts` (`fb386074f5`). None touches a file in this diff, and a no-driver `merge-tree` of the head onto `1378ec7c0c` exits 0. So there was no merge. The open-PR file lists were re-read at 00:18Z: 11 open PRs, none touching a file in this diff. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…/ searchFields (objectstack-ai#20776) Fixes objectstack-ai#20500 Clause-②: no ## What changed Two files, three places, all inside the claim surface (claim 5903909164 with amendments 5904148643 and 5905295272). In `skills/objectstack-query/SKILL.md`, the Calling Convention section: (1) The engine `aggregate` row (`:26`): the legal option keys now list `search` and `searchFields` after `timezone`, with one clause saying the two search keys filter the input rows **before** grouping, AND-ed with `where`, exactly as on `find`. (2) The passthrough paragraph (`:38-44`): it said all six driver passthrough keys are deliberately ILLEGAL on `count` and `aggregate`; `timezone` is one of the six and is a legal option on `aggregate` in its own right (in `ENGINE_AGGREGATE_OPTION_KEYS`, read by `aggregate()` for date bucketing), which the row above already showed. The paragraph now carves that one key out: `aggregate` reads it itself, so it is legal there and the row lists it; `count` refuses it with the rest. (3) In `skills/objectstack-query/rules/aggregation.md:49` (round 3, `cc924448`): "`fields` is not one of the six keys `engine.aggregate()` accepts" no longer states a count — "not one of the keys `engine.aggregate()` accepts" — so it cannot contradict the row it cites, which now lists eight. Net +2 lines across the three commits (`aggregation.md` net 0). Nothing else moved: the closed-set sentence at `SKILL.md:31` and the `fields` / `orderBy` sentence (now `:254`) stay, both still true. Landing sites: the row the dispatch expected (`:26`); the `:38-44` paragraph the seat added after the first report's out-of-scope finding; and `rules/aggregation.md:49`, which the at-tier contract review of `a87b7380` (5904504985, FAIL) found — a sentence that names a count, invisible to the first census's key-name probes. The `content/docs/**` half of the census (two docs sentences that enumerate the same set) is carded as objectstack-ai#20792, ⛔ not in this PR. ## In-place fix (patch round, `a87b7380`) In-place fix under the bounded exemption (claim amendment 5904148643), all four conditions holding: ① the same defect class as the card — the skill misstated the legal `aggregate` option set (the `:26` row understated it; the `:38-42` paragraph overstated the illegal set by one key); ② mechanical, in an already-pinned form — the carve-out names `timezone` as the one passthrough key `aggregate` accepts and reads; ③ no other claim holds the file (no open PR touches `skills/objectstack-query/**`, per the claim's serial-constraints reading); ④ the same gate family — the list derived at `a87b7380` is the same 24 commands as at `f997cf8e`, no new verification surface. Measured on `origin/main` `c9c182ed` before writing: `ENGINE_COUNT_OPTION_KEYS` (`packages/objectql/src/engine.ts:556`) is `context`, `where`, and `count()` rejects against it (`:16168`), so `timezone` is refused on `count`; `ENGINE_AGGREGATE_OPTION_KEYS` (`:562-565`) contains `timezone` and none of `transaction`, `tenantId`, `tenantIds`, `bypassTenantAudit`, `preserveAudit` (0 hits each); `aggregate()` spans `:16267-16686` and reads `const tz = query.timezone` at `:16598`. The replacement sentence reuses the file's own vocabulary ("date bucketing", `:89` and `:257`; "the row above"). ## Why Since PR objectstack-ai#20487, `ENGINE_AGGREGATE_OPTION_KEYS` (`packages/objectql/src/engine.ts:562` at `c9c182ed`) is `context`, `where`, `groupBy`, `aggregations`, `having`, `timezone`, `search`, `searchFields`. The row stopped at `timezone`, and together with the closed-set sentence it told an agent that a searched aggregate is refused. An agent reading that would group a searched page on the client, which is the wrong-number workaround objectstack-ai#20358 retired: group counts over a page window instead of over all searched rows, with no error. ## Behaviour verified at the code, not at the comment `aggregate()` (`engine.ts:16267-16277`): `rejectUnknownEngineOptions(object, 'aggregate', query, ENGINE_AGGREGATE_OPTION_KEYS)` admits both keys, then `expandSearchOnAggregateOptions` (`:11104-11114`) builds a carrier AST from `where` + `search` + `searchFields` and runs the one ADR-0061 expander `find` uses, `expandSearchOnAst` (`:11062-11086`): each term becomes an `$or` of `$icontains` over the resolved searchable fields, the result is AND-ed with the caller's `where` (`{ $and: [where, searchFilter] }`), and the two search keys are deleted from the bag. All of that runs before the aggregate AST is built and before any grouping, so the grouped answer is the grouping of the searched rows, which is what the new clause says. ## Census (list-shaped probes, as the dispatch asked) At `c9c182ed`, over `skills/` and `content/docs/`: - lines naming both `having` and `timezone`: 1 hit, `skills/objectstack-query/SKILL.md:26` (the row corrected here); - lines naming `groupBy`, `aggregations` and `having` together: 2 hits, the same row and `content/docs/kernel/runtime-services/data-service.mdx:91`, which lists the `QueryAST` clauses (it already names `search`; it is not an enumeration of the engine option set, so no edit); - control, `ENGINE_AGGREGATE_OPTION_KEYS`: 2 hits, `SKILL.md:31` and `:254` (`:252` before this PR), both prose sentences, both still true; - control, the engine `aggregate` row spelling: 1 hit, `:26`. `skills/objectstack-query/rules/aggregation.md:104` already says `where` filters the input rows before grouping, the vocabulary the new clause reuses. **Round-3 census (count-shaped probes, after the review).** Over all six files of `skills/objectstack-query/**`: number words, `only` near `accept` / `key` / `option`, `keys` near `accepts` / `legal` / `closed set`, and list-shaped rows, with the literal `engine.aggregate()` as control — the one stale statement was `rules/aggregation.md:49` ("six keys"), fixed here; every other number word counts a set of that size. Over `content/docs/**`: two sentences enumerate the aggregate set falsely — `content/docs/protocol/objectql/query-syntax.mdx:1337` and `content/docs/data-modeling/queries.mdx:672` — carded as objectstack-ai#20792 with the evidence and a proposed patch; they are outside this PR. The full probe list and hit counts are in the dev report 5904713155. ## The two `skills/**` readings Tokens in the ratchet's own convention, `ceil(utf8 bytes / 4)`. | Reading | Before (`c9c182ed`) | After (`cc924448`) | Delta | |:--|:--|:--|:--| | touched file `skills/objectstack-query/SKILL.md`, lines | 399 | 401 | +2 | | touched file, tokens (ceiling 5552) | 3915 | 3990 | +75, headroom 1562 | | touched file `skills/objectstack-query/rules/aggregation.md`, lines | 241 | 241 | 0 | | touched file, tokens (ceiling 2357) | 1846 | 1845 | −1, headroom 512 | | whole package `skills/**` (every file), lines | 13424 | 13426 | +2 | | whole package `skills/**` (every file), tokens | 155899 | 155973 | +74 | | all ten `SKILL.md`, lines | 4404 | 4406 | +2 | PM line budget for this card: net +2 lines at most; +2 used (0 by the row commit `f997cf8e`, +2 by the paragraph commit `a87b7380`, 0 by `cc924448`), not exceeded. No existing line was re-wrapped: `SKILL.md:38-41` are byte-identical to `main`, `:42` was extended in place and two lines follow it; `rules/aggregation.md:49` is replaced in place. ## Changeset `skip-changeset`, by measurement: no released package's `files[]` names a `skills` path (every `packages/**/package.json` scanned; positive control: `create-objectstack` lists `dist`, `README.md`, `CHANGELOG.md`). Scaffolded projects pull the catalog from the repository path `objectstack-ai/objectstack/skills` through the `skills` CLI (`packages/create-objectstack/src/skills-install.ts:62`), never from an npm tarball. ## Gates **Head `cc924448` (round 3):** the same 24 derived families plus `check:skill-refs` ran green on `cc924448` (dev report 5904713155); their `--ran` reconciliation is carried by the report's 47-family superset over the round's working tree, not by a 24-only run. CI on `cc924448`: 31 check-runs — 23 `success`, 7 path-filtered `skipped`, 1 `failure` (`Check Changeset`, the missing `skip-changeset` label; see *Changeset*); `Lint & Repo Gates`, `TypeScript Type Check` and `Type Check · source gates` (the token ratchet and the skill-doc gates) `success`. The table below is the round-2 record on `a87b7380`. Derived from the actual change with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at head `a87b7380` (24 commands, the same list as at `f997cf8e` and as the dispatch), all run on head `a87b7380`, exit codes captured before any pipe, reconciled with `--ran`. | Gate (as `--commands` printed it) | Exit at `a87b7380` | |:--|:--| | `node scripts/check-ci-filter-parity.mjs` | 0 | | `node scripts/check-closing-keyword-parity.mjs` | 0 | | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | | `node scripts/check-comment-mask-corpus.mjs` | 0 | | `node scripts/check-doc-route-spelling.mjs --advisory` | 0 | | `node scripts/check-doc-route-spelling.mjs --self-test` | 0 | | `node scripts/check-skills-token-ratchet.mjs` | 0 | | `node scripts/check-skills-token-ratchet.mjs --self-test` | 0 | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 | | `pnpm --filter @objectstack/spec run check:skill-docs` | 0 | | `pnpm check:agent-test-spelling` | 0 | | `pnpm check:corpus-claim-drift` | 0 | | `pnpm check:cross-package-test-inputs` | 0 | | `pnpm check:doc-authoring` | 0 | | `pnpm check:driver-memory-census` | 0 | | `pnpm check:gitlink-declared` | 0 | | `pnpm check:nul-bytes` | 0 | | `pnpm check:pm-governed-merges` | 0 | | `pnpm check:refd-timer-probe` | 0 | | `pnpm check:role-word` | 0 | | `pnpm check:skill-compatibility` | 0 | | `pnpm check:skill-frame-sync` | 0 | | `pnpm check:skill-identifier-liveness` | 0 | | `pnpm check:watch-hint-literal` | 0 | | `pnpm --filter @objectstack/spec run check:skill-refs` (extra, not derived; AGENTS.md names it for a `SKILL.md` edit) | 0 | `--ran` reconciliation at `a87b7380`: 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN (`dispatch-gates --ran`, exit 0). Ratchet family: `check-skills-token-ratchet` prints `skills/objectstack-query/SKILL.md is 3990 tokens (ceiling 5552; headroom 1562)`. `check:doc-formula-expressions` needs `@objectstack/formula` and `@objectstack/lint` built; they were built under `os-verify-lock.sh` before the gate ran (turbo cache hit, VERDICT command-exit 0), so this round it passed on its first run. At the earlier head `f997cf8e` the same 24 were green too (that round's first `check:doc-formula-expressions` run exited 3 for the missing build, NOT MEASURED, and passed after the build). `check:skill-docs` reads frontmatter only (`packages/spec/scripts/build-skill-docs.ts`), so no generated artifact moves for a body edit; `skills/README.md` and `content/docs/ai/skills-reference.mdx` stay byte-identical. No package build or test is owed: the diff touches no package (`turbo ls --affected` is blind here by construction — the skill lives outside the package graph). A repo-wide `pnpm lint` (`eslint . --no-inline-config`) is CI-owned; it was not run locally and is not claimed (NOT MEASURED locally, CI reports it). Population reading from eslint's own configuration: `pnpm exec eslint --print-config skills/objectstack-query/SKILL.md` prints `undefined` (no config block matches the file), and every `files:` glob in `eslint.config.mjs` is a `{ts,tsx,mts,cts,js,jsx,mjs,cjs}` pattern, so the one edited file is outside eslint's population and this diff cannot move any lint verdict on any file. ## Acceptance notes - The first round's one out-of-scope observation — `SKILL.md:38-42` said all six driver passthrough keys are illegal on `aggregate`, while `timezone` is legal and read there — is fixed in this PR by the patch round `a87b7380`, under the bounded in-place exemption (see *In-place fix* above). - The `content/docs/**` half — `query-syntax.mdx:1337` and `queries.mdx:672` enumerate the aggregate option set as "only `where` / `groupBy` / `aggregations`" — is carded as **objectstack-ai#20792** (filed for triage; `domain:devx` by the lane table), carrying the evidence and the round-3 dev's proposed patch. `Fixes objectstack-ai#20500` closes the card over the `skills/**` half, which is the card's own defect; the docs half lives on objectstack-ai#20792. - Noted, not carded: the doc comment on `ENGINE_DRIVER_PASSTHROUGH_KEYS` (`packages/objectql/src/engine.ts:507-513`) still says the six keys are "deliberately NOT legal" on `count` / `aggregate` — stale by `timezone` on `aggregate`; a code comment, not a shipped surface (carrier: the next edit of that block). ## 维护者速读(草稿) - **改了什么**:`skills/objectstack-query` 技能里「Calling Convention」表的 engine `aggregate` 一行,合法选项键补上 `search`、`searchFields`,并加一句:这两个键在分组之前过滤输入行,与 `where` 取交集,行为与 `find` 一致。另将同段落里「六个直通键在 count / aggregate 上一律非法」的表述修正为 timezone 例外(aggregate 自己读取它做日期分桶,count 仍拒绝);`rules/aggregation.md` 里「six keys」这一数字去掉(表里已是八个键)。技能包净 +2 行。`content/docs` 里同样写错的两句另立 objectstack-ai#20792,不在本 PR。 - **为什么改**:PR objectstack-ai#20487 落地后,运行时的 `aggregate` 已经接受并执行 `search` / `searchFields`,技能却还写着不接受。AI 读了这份技能会绕路:先查一页再在客户端分组,得到的分组计数只覆盖一页而不是全部命中行,且没有任何报错。写给 AI 的技能说错一句等于产品缺陷。 - **风险与代价(含回滚)**:纯文档改动,不动代码、不动发布包(`skills/**` 不在任何 npm 包的 `files[]` 里,脚手架项目从仓库路径拉取)。token 棘轮上限未动(SKILL.md 3915 → 3990 / 5552;aggregation.md 1846 → 1845 / 2357)。回滚即 revert 本 PR。 - **席位意见**: - **你要做的**:确认这一行的措辞,批准后由席位落地(受管面 Tier H,草稿 PR)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20358
Clause-②: yes
What this does
A grouped or aggregated query now honours ADR-0061
search. Route (A) from triage (5871414670):@objectstack/spec(packages/spec/src/data/data-engine.zod.ts):EngineAggregateOptionsSchemadeclaressearchandsearchFieldswith the same Zod expression asEngineQueryOptionsSchema(z.union([z.string(), FullTextSearchSchema]),z.array(z.string())). The structuredsearcharm carries flag defaults, so the aggregate options now have two shapes. ADR-0122 andcheck:spec-parsed-aliastherefore requireEngineAggregateOptionsParsed. It is declared, and its isomorphism pin leavestype-alias-convention.pin.test.ts(780 to 779). That is the one added export:check:api-surfacereports 0 breaking, 1 added.@objectstack/objectql(packages/objectql/src/engine.ts):ENGINE_AGGREGATE_OPTION_KEYSgains the two keys.aggregate()runs them through the existingexpandSearchOnAst, via a small carrier helper (expandSearchOnAggregateOptions), and does not add a second expander. The expansion sits at the same point in the sequence as onfind: after thewheredoors (lowerWhereFilterArray), and before the AST is built, tokens resolve and the security middlewares run. The helper returns a copy of the caller's bag, so the bag is never written through.@objectstack/metadata-protocol(packages/metadata-protocol/src/protocol.ts):findData's grouped branch passessearch/searchFieldstoengine.aggregate.searchFieldswas already validated before the branch fork (the INVALID_FIELD gate), so both branches refuse the same bad override.Mechanism hypotheses, measured:
EngineAggregateOptionsSchemais read.DataEngineAggregateOptionsSchema(the deprecated legacy schema) has no runtime reader:git grepfinds it only inpackages/spectests and the type-alias pin. It is left unwidened.searchon aggregate (rejectUnknownEngineOptions), confirmed.Landing site beyond the three declared files: the public-door pin went into
packages/rest/src/list-view-grouping-query-door.test.tsas a new §10. That file already drives the card's 186-row fixture through the realRestServer→findData→ObjectQL.aggregate→ sqliteSqlDriverchain on both aggregate tiers. The pin is test-only, and no second harness was written.Before / after at
POST /api/v1/data/:object/queryBefore (reproduced by reverting only the protocol pass-through, which is the base's door behaviour; the engine change is inert when the key is not sent):
{ groupBy: ['business_unit'], aggregations: [count], search: 'harbour' }answered five groups,northgate_operations 86 / northgate_quality 61 / riverside_plant 31 / northgate_plant 7 / harbour_office 1, the unsearched answer. On both tiers, the flat{ search: 'harbour' }returned one row.After:
[{ business_unit: 'harbour_office', count: 1 }],total: 1, on both tiers.Tests
Head of this PR:
2196566d3f. The full suites ran on3576fd34e7(this branch withorigin/mainacd009521emerged in). The two commits after it change onlypackages/objectql/src/engine-aggregate-search.test.ts: the find double now applies the caller'slimit(check:objectql-double-limit), and threeas anyerasures became typed calls (check:query-options-erasure). That file and objectql's typecheck were re-run on2196566d3f. All vitest runs used--maxWorkers=2.@objectstack/objectqlvitest run --project local@objectstack/objectqlvitest run --project repo@objectstack/objectqltypecheck(tsc + scripts + test layer)2196566d3f@objectstack/objectqlsrc/engine-aggregate-search.test.tson2196566d3f@objectstack/metadata-protocolvitest run@objectstack/metadata-protocoltypecheck@objectstack/restvitest run --project local@objectstack/restvitest run --project repo@objectstack/resttypecheck(tsc + test layer)@objectstack/specvitest run --project local@objectstack/specvitest run --project repo@objectstack/spectypecheck(tsc + scripts + test layer)The filter direction is the changed packages themselves.
@objectstack/restis included because it hosts the public-door pin and its route tests cover grouped queries. The downstream consumers of the widenedEngineAggregateOptionstype are covered by the ones that compile against it here (objectql, metadata-protocol and rest typecheck);check:api-surfaceshows 0 removed or narrowed.New pins:
packages/rest/src/list-view-grouping-query-door.test.ts§10, the public door, both aggregate tiers (driver-sql nativeGROUP BY, and the in-memory lowering). Each tier assertion checks which face served the query.search: 'harbour'returns the ONE group,total: 1, and equals the grouping of the flat searched rows. The in-memory tier's rows read carries$icontainsand nosearchkey.search: 'northgate': every header number,sum_amountincluded, equals the grouping of the flat searched rows.searchFields: ['owner']narrows the answer to theowner_3rows. The oracle is the fixture generator, not the door. The one-row unit drops out, and the narrowed answer differs from the default-field answer.search+having: search dropsriverside_plant, andhavingdropsnorthgate_plant.aggregationswith nogroupBy: 154, where the unsearched control gives 186.searchFieldsis400/INVALID_FIELD.packages/objectql/src/engine-aggregate-search.test.ts, both tiers:find()'s searched rows (default fields, narrowed, structured form, AND-ed withwhere, multi-term);wherethe driver receives fromaggregatedeep-equals the onefindsends for the same bag, and neither AST carriessearch/searchFields;where;groupByaggregations,search+having, and the caller's frozen bag is not written through;$search/$searchFieldsare still refused on aggregate, as unknown options;ENGINE_OPTION_KEY_SETS.aggregateexactly, and every legal key must have an observable effect. This is the questionfindOne's drift pin asks, now asked of this verb.packages/spec/src/data/data-engine.test.ts: a parse keeps both keys in bothsearchforms, and the aggregate options refuse the values the query options refuse. The two keys' input JSON Schemas deep-equalEngineQueryOptionsSchema's (one declaration on both verbs).engine-unknown-option.test.ts(each legal set equals its schema's shape) holds unchanged. So does the unknown-key refusal pin foraggregate('bogus'), byte for byte.Reverse verification (one-shot, not left in the tree)
Both ablations committed the fix first, then mutated through
scripts/ablation-replace.mjs(literal anchor, on-disk counts and blob hashes). Each armed an absolute-path restore trap, and each restore was proven by blob equality withHEADand an emptygit diff HEAD.dist/.search: options.search,+searchFields: options.searchFields,was deleted, taking the blob from508f88c8c8e0tof8a47dbaf791.@objectstack/metadata-protocolwas rebuilt, andablation-dist-preflight --absentconfirmed the marker was gone from all 24 built files.list-view-grouping-query-door.test.ts: 10 failed / 34 passed. Every positive §10 case failed on both tiers, in the predicted direction: the measured case received the five unsearched groups, 86/61/31/7/1. ThesearchFieldsrefusal case stayed green, because that gate sits before the branch fork../engine.js).engine-aggregate-search.test.ts: 13 failed / 4 passed. The 12 behavioural cases failed across both tiers, and so did the drift pin, on itssearchproof, which is the declared-but-unexecuted shape it exists to catch. The 4 that stayed green were the frozen-bag case and the two$searchrefusals, none of which depend on the expansion.177d256d61d4equalsHEAD, andgit diff HEADis empty.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 114 commands. All 114 were run on2196566d3f, each exit captured before any pipe and reconciled with--ran:114 derived, 112 run, 2 NOT-MEASURED, 0 UNRUN. 111 exited 0, includingcheck:spec-parsed-alias,check:api-surface,check:authorable-surface,check:docs,check:export-origins,check:generated,check:liveness,check:query-options-erasure,check:objectql-double-limit,check:where-matcher,check:engine-double-contract,check:adr-0087-registration,check:changeset-no-majorandcheck:nul-bytes.check:skill-examplesrefused its prerequisite on the first run (client-react not built). It was run after building@objectstack/client-react..., and 259 prose examples type-check (exit 0).check:dual-build-cjs-loads, reason: exit 3, PREREQUISITE NOT MET (it needs every package'sdist/; CI builds them).check:type-check-debt, reason: my 420 s timeout fired mid re-measure (exit 124). It needs the whole./packagesclosure built, whichlint.ymldoes first.check-engine-split-ratio --days 90, reason: the clone is shallow and the gate refused (exit 2) rather than compute over a truncated window.Spec artefacts, regenerated only where
check:generatedproved them stale:api-surface/data.json(+EngineAggregateOptionsParsed: 0 breaking, 1 added);export-origins/data.json;content/docs/references/data/data-engine.mdx;authorable-surface/data.json(+2 rows, written by the spec build'sgen:schema).After the merge of
origin/main,check:generatedreported all 15 artifacts up to date against a freshly builtpackages/spec/dist.Acceptance notes
skills/objectstack-query/SKILL.md, the Calling Convention table lists engineaggregate's legal keys ascontext,where,groupBy,aggregations,having,timezone, and after this PR the closed set also holdssearchandsearchFields. The row understates the set: nothing it names is refused, but a reader would not knowsearchworks.skills/**is a Tier H governed surface and outside this card's declared file surface, so the one-row edit (0 net lines) is left for the seat to route. It is raised in the dev report.findData's grouped branch still spells the bagas any. Every key in it is now declared, so the erasure could go. Dropping it movesprotocol.ts's count in the shrink-onlyscripts/query-options-erasure-baseline.json(6 to 5), which is outside this card's surface. Carrier: whoever next touches that branch.Error, with no ADR-0112code/status. This is pre-existing and unchanged byte for byte.findDatanever builds the aggregate bag from caller keys, so the refusal is not reachable fromPOST /data/:object/query, and it stays pinned by message as before.engine.countstill takes nosearch. So the flat branch undersearch+limitstill reports a page-localtotalestimate, which is documented behaviour and unchanged here.compileListViewGroupQuerydoes not carrysearchitself; a client spreadssearchonto the compiled body, as §10 does. That card is the accepting seat's to file, withBlocked-by:on this one.DataEngineAggregateOptionsSchemais deliberately not widened. It is the deprecated legacy aggregate schema, with no runtime reader (measured bygit grep:packages/spectests and the type-alias pin only).Generated by Claude Code