Skip to content

fix(objectql,metadata-protocol,spec): a paged search reports the counted total - #22857

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22790-search-total-count
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22790-search-total-count

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22790
Clause-②: yes (widening)

What this does

Under a search, a paged list's total is now the counted number of matches, and hasMore agrees with it. This is the triage direction (6106471041), executed as ruled: one expander serves both verbs. There is no second expander, no omitted total and no estimate marker.

  • @objectstack/objectql (packages/objectql/src/engine.ts): ENGINE_COUNT_OPTION_KEYS admits search / searchFields. count() runs them through the existing expandSearchOnAst, via the carrier helper the aggregate verb already used, now named expandSearchOnOptions and typed for both bags. That is the expansion find runs, with its internal: true exclusion, its searchableFields / nameField resolution and its searchFields narrowing. It sits at the same point in the sequence as on find and aggregate: after the where doors, before the AST is built, tokens resolve and the security middlewares run.
  • @objectstack/metadata-protocol (packages/metadata-protocol/src/protocol.ts): findData's paged arm passes search / searchFields to engine.count(). The countable exception and its page-local estimate arm are deleted. $count=false is unchanged. The degraded-count catch is untouched (out of scope per triage).
  • @objectstack/spec (packages/spec/src/data/data-engine.zod.ts): EngineCountOptionsSchema declares search and searchFields with the same Zod expressions as EngineQueryOptionsSchema. The structured search arm carries flag defaults, so the author and parsed states part. EngineCountOptionsParsed is therefore declared (ADR-0122, check:spec-parsed-alias), and the count schema's isomorphism pin leaves type-alias-convention.pin.test.ts (776 to 775). check:api-surface: 1 added, 0 removed or narrowed.

Before / after at the public door

packages/rest/src/list-search-total-door.test.ts (new) drives the RestServer route handlers, findData, ObjectQL and a sqlite SqlDriver over 90 rows. 75 rows match Line: 60 in title and 15 in notes. 15 more match only in the internal: true column hint. A middleware scopes the member to every third row, which is 25 of the 75 matches.

request before this PR
admin GET ?$top=25&$search=Line total: 26, hasMore: true total: 75, hasMore: true
admin GET ?$top=25&$skip=50&$search=Line (last page) total: 76, hasMore: true total: 75, hasMore: false
admin POST …/query { search: 'Line', limit: 25 } total: 26 total: 75
admin POST …/query with searchFields: ['title'] not reached total: 60; the unpaged rows are 60
member GET ?$top=10&$search=Line total: 11 total: 25
CONTROL: admin / member GET ?$top, no search 90 / 30 90 / 30
CONTROL: $count=false with a search total omitted total omitted

The first-page, POST and member "before" figures were measured on base c4e7fa5a31. The last-page figure comes from ablation A1 below, which restores the protocol's estimate arm on this branch. That is the base door's behaviour, because the engine change is inert when the count is sent no search.

Mechanism hypotheses, measured

  1. The PM's reading held on c4e7fa5a31: protocol.ts had const countable = options.search == null; and the estimate arm (line 13321). ObjectQL.count was gated by rejectUnknownEngineOptions(…, ENGINE_COUNT_OPTION_KEYS) over { context, where } (line 18696). find's expansion was expandSearchOnAst (line 12632), with collectInternalReadFields.
  2. The Clause-② arm is yes (widening). No route exists without the spec. The drift pin in engine-unknown-option.test.ts holds ENGINE_OPTION_KEY_SETS.count equal to EngineCountOptionsSchema's shape, with no documented extras on count. Admitting search engine-side without declaring it reds that pin. The remaining routes are a bypass of rejectUnknownEngineOptions or a private side channel, and both are ruled out.
  3. Both verbs use the same expansion, measured. The driver's COUNT receives a where deep-equal to the driver's FIND where for the same request. The count equals the unpaged row count on the default set (75, with the internal column excluded), on the narrowed set (60) and in the member's scope (25).
  4. Engine doubles: check:engine-double-contract and check:objectql-double-limit exit 0. No double changed, because the new pin uses the real engine.

searchFields is declared beside search, deliberately. findData hands find both keys, and on stored-metadata tables it narrows searchFields itself (narrowStoredMetadataSearch). A count that took search alone would count over the wider default set, so total would exceed the rows paged. On a stored-metadata table it would also count matches in the withheld body and hash columns.

Landing beyond the declared file surface

The claim named three source files. The diff also touches the following files, and the widening forces each one:

  • packages/spec/src/data/hook-api.ts (TSDoc only) and hook-api.test.ts. HookCountQuery is an Omit of EngineCountOptions, so it gains the pair by derivation. Its TSDoc said "where and nothing else" and quoted ENGINE_COUNT_OPTION_KEYS as { context, where }. Both became false with this change, and both are corrected. The test's drift pin ("each derived shape equals KEPT ∪ OMITTED") forces the decision: the pair is KEPT, because ctx.api.object(n).count() forwards its bag to engine.count() and now honours it. This is a spec source file beyond the [PM seat] domain:spec — ⏳ vacant #6017 declaration (6108690316), and it is flagged to the PM in the report.
  • packages/spec/src/type-alias-convention.pin.test.ts: the isomorphism pin leaves (776 to 775).
  • packages/spec/src/data/data-engine.test.ts: a new case checks that the count options declare the pair exactly as the find options do (JSON-schema equality, both search forms).
  • Two tests pinned the old estimate and are flipped: protocol.zero-limit-total.test.ts (metadata-protocol) and protocol-data.test.ts (objectql).
  • Four artifacts were regenerated with the repo's tooling, the ones check:generated named: api-surface/data.json, authorable-surface/data.json, export-origins/data.json and content/docs/references/data/data-engine.mdx.
  • The public-door pin, packages/rest/src/list-search-total-door.test.ts.

Tests

package run result
@objectstack/objectql vitest run --project local 399 files, 7802 passed
@objectstack/objectql vitest run --project repo 1 file, 5 passed
@objectstack/objectql typecheck exit 0
@objectstack/metadata-protocol vitest run 225 files (+3 skipped), 28129 passed, 19 skipped
@objectstack/metadata-protocol typecheck exit 0
@objectstack/rest vitest run --project local 273 files, 5251 passed, 326 skipped
@objectstack/rest vitest run --project repo 5 files, 209 passed, 1 skipped
@objectstack/rest typecheck exit 0
@objectstack/spec vitest run --project local 648 files, 19343 passed, 1 todo
@objectstack/spec vitest run --project repo 55 files, 971 passed
@objectstack/spec typecheck (tsc, scripts and test layer; the three edited spec tests are in the test-layer program) exit 0

The filter direction is the changed packages themselves, plus @objectstack/rest, which hosts the door pin and compiles against the widened type.

Ablations (one leg per negative pin, plus the two halves of the fix). In each leg, scripts/ablation-replace.mjs makes the mutation: the anchor must hit, the blob change is verified, and the restore checks that the blob equals HEAD and that git diff HEAD is empty. The package is then rebuilt and ablation-dist-preflight.mjs finds the marker in dist/. The door pin runs, the package is rebuilt again, and the preflight confirms with --absent that the marker is gone and the tree is clean.

leg mutation what went red
A1 protocol: the estimate arm restored under a search 4 of 6: GET total 26 (want 75), last page 76 / hasMore: true; POST 26; one-expander (no COUNT issued); member 11 (want 25). Both controls stayed green.
A2 protocol: $count=false ignored the $count=false control (total present)
A3 engine: the internal: true exclusion removed from the shared expander unpaged rows 90 (want 75), POST total 90, member 30
A4 engine: the count's driver call built from the caller's where instead of the middleware-scoped AST member 75 (want 25), member control 90 (want 30)
A5 engine: the count's search expansion removed GET / POST totals 90, last page hasMore: true, one-expander, member

Two first attempts did not run, and both were re-run:

  • The first A1 attempt was a no-op. Its replacement contained its anchor, so ablation-replace.mjs refused it (anchor count 1 to 1) and restored the file. It was re-run with a distinct anchor.
  • The first A3 attempt never ran its test. The marker's source spelling carries a string[] annotation that dist/ drops, so the preflight's tree reading refused (exit 91). It was re-run with --source-marker.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack re-derived 116 commands for this diff: the dispatch's 78, plus 38 from the changeset, docs, engine-double, query-options-erasure and type-check-coverage families, among others. Every one was run with its exit code recorded. The --ran reconciliation answers 116 derived, 115 run, 1 NOT MEASURED, 0 unrun.

  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET. 44 packages outside this diff's build closure have no dist/. A narrowed probe loaded every CJS entry of the three changed packages with 0 failures: spec 19, objectql 2, metadata-protocol 1.
  • pnpm --filter @objectstack/spec run check:skill-examples first exited 3, because the client SDK was not built. After building @objectstack/client-react's closure it exits 0 (262 examples).
  • The derivation printed STALE TREE: the tree is 5 commits behind origin/main, and 7 derivation inputs changed there. The gates ran on this branch's tree.

Lint is narrowed here; CI owns the repo-wide run. eslint --no-inline-config --format json over the 10 touched .ts files reports 10 files, 0 errors and 0 warnings. Population: all 10 resolve a config (--print-config). Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move a verdict on an untouched file.

Line budget: +301 / -62 across 15 files, 25 of the added lines generated. The four source files account for +61 / -42, mostly corrected or replaced comment text. The door pin (157 lines) exceeds the suggested 300 / 60 on its own: it carries a real-chain harness, a member scope and the driver capture.

Acceptance notes

  • The degraded-count catch in findData, which reports a failed COUNT as offset + page, is untouched per triage. It was not measured reachable here.
  • DataEngineCountRequestSchema (the RPC request shape) extends EngineCountOptionsSchema, so its generated doc rows gained the pair too. git grep finds no runtime reader of that schema outside packages/spec.
  • objectui#12123's footer check needs no change. Once this lands, its page-labelled fallback no longer fires under a search.
  • Clause-② is yes, so this PR owes one contract-review-tier review before the queue. The seat arranges it.

Generated by Claude Code

…ed under the search

engine.count() takes the query's search / searchFields and expands them
through the one ADR-0061 expander find runs (expandSearchOnOptions, shared
with aggregate); findData passes them to the count and the countable
page-local estimate arm is gone. EngineCountOptionsSchema declares the pair
exactly as EngineQueryOptionsSchema does, with EngineCountOptionsParsed.

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 12b9daf7493f30ebb5faf47e7b40eb99d2bda267.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/data.json, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 72 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 12b9daf7493f30ebb5faf47e7b40eb99d2bda267 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from af2bfae226ea4a123eb866566815c3a96eec2ac2 — the merge of head 4e057d908e16aae99375c697f7420e789ffc5344 into base 12b9daf7493f30ebb5faf47e7b40eb99d2bda267, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin af2bfae226ea4a123eb866566815c3a96eec2ac2 && git checkout af2bfae226ea4a123eb866566815c3a96eec2ac2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 12b9daf7493f30ebb5faf47e7b40eb99d2bda267 4e057d908e16aae99375c697f7420e789ffc5344 && git checkout -B drift-repro 12b9daf7493f30ebb5faf47e7b40eb99d2bda267 && git merge --no-ff 4e057d908e16aae99375c697f7420e789ffc5344

node scripts/docs-audit/affected-docs.mjs --json 12b9daf7493f30ebb5faf47e7b40eb99d2bda267

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4e057d908e16aae99375c697f7420e789ffc5344
Local-runs: none

Read at 2026-10-11T14:33Z on card #22790 (triage 6106471041, claim 6108682129, the os-dev-report), the two #6017 declarations (6108690316, 6110016343), PR #22857's body, file list and net diff against main at the merge-base c4e7fa5a31, the #20487 precedent where it declared the pair on EngineAggregateOptionsSchema, and the check-runs on this head. Read-only: git diff / git show / git grep / git merge-tree against fetched shas, gh api reads; nothing built, run or re-run.

① Derived judgments

  • One expander — right. count() (engine.ts 18691) runs foldEngineOptionAliases → rejectUnknownEngineOptions → lowerWhereFilterArray → expandSearchOnOptions (12700), which carries { object, where, search, searchFields } into the one expandSearchOnAst (12633) that find calls at 12859 and aggregate reaches through the same helper at 18837. Same schema source (this._registry.getObject(object)), same collectInternalReadFields withhold of internal: true columns, same searchableFields / nameField ?? displayNameField resolution, same requestedFields = searchFields ?? raw.fields narrowing intersected never widened. Sequence position matches aggregate exactly (after the where doors, before the AST, token resolution and middleware); find differs only in that it runs fillQueryAstDefaults first, and that is inert for search, which is deliberately not parsed (12743). A searchFields-only bag or a null search takes the same no-expansion path on every verb. The helper is the feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search #20487 expandSearchOnAggregateOptions generalised over T extends EngineAggregateOptions | EngineCountOptions; no second expander exists, and the door pin's one-expander case asserts the driver COUNT where deep-equals the driver FIND where.
  • Middleware-scoped count — right. opCtx.ast = { object, where: query?.where } is built from the expanded bag (18709), resolveWhereTokens runs on it, the security middlewares inject into opCtx.ast.where, and driver.count receives opCtx.ast (18734). The find().length fallback passes callerWhere (captured after expansion, before middleware, 18725) with no search key, so find scopes it itself and never re-expands. Door pin: member sees total: 25 of 75; dev ablation A4 went red in the predicted direction.
  • findData paged arm — right. const countable = options.search == null and the else estimate arm (total = pageOffset + records.length + (hasMore ? 1 : 0)) are deleted. The count call carries where, search, searchFields, context — the same options.search / options.searchFields the find call received, after assertSearchFieldsAreSearchable and narrowStoredMetadataSearch (13005–13023), so the count narrows exactly as the rows do. hasMore = pageOffset + records.length < counted agrees with total by construction. The countOptOut arm is unchanged (total = undefined, hasMore from the page). The degraded-count catch (counted = pageOffset + records.length) is byte-unchanged, per triage. The two tests that pinned the estimate are flipped to the counted shape, and the zero-limit pin now asserts the COUNT carries the same search the FIND did.
  • Schema members — right. search: z.union([z.string(), FullTextSearchSchema]).optional() and searchFields: z.array(z.string()).optional() are byte-identical to EngineQueryOptionsSchema (144, 153) and to the feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search #20487 declaration on EngineAggregateOptionsSchema (448, 454). data-engine.test.ts pins JSON-schema equality of both keys against the query options. EngineCountOptionsParsed = z.infer is declared beside EngineCountOptions = z.input because FullTextSearchSchema carries flag defaults, so input and infer part — ADR-0122's condition — and the isomorphism pin Iso_data_dataEngine__EngineCountOptionsSchema leaves (776 → 775), the same shape feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search #20487 took (780 → 779). The deprecated DataEngineCountOptionsSchema is left unwidened, as feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search #20487 left its legacy sibling. Generated: api-surface +1 type, authorable-surface +2 keys, export-origins +1, data-engine.mdx rows; nothing removed or narrowed.
  • ENGINE_COUNT_OPTION_KEYS and the refusal — right. The set is { context, where, search, searchFields }; rejectUnknownEngineOptions is untouched. The drift pin engine-unknown-option.test.ts:264 (expectSetMatches('count', EngineCountOptionsSchema, [])) derives the legal set from the schema with no documented extras, so set and schema stay equal — and that pin is what makes the spec the only route, which is the PR's Clause-② argument; it holds. No test pins the old count legal-key message (none found by git grep). The bogus / tenantId refusal pins for count are unchanged.
  • HookCountQuery — right. Omit<EngineCountOptions, 'context'> widens by derivation. Both TSDoc sites are corrected (the method-group note and the type's own doc) and both now read true against the engine constant. hook-api.test.ts KEPT ['where','search','searchFields'] / OMITTED ['context'] equals the Omit. The runtime face honours it: packages/runtime/src/sandbox/body-runner.ts:995 forwards the bag whole to ql.count(objectName, withCtx(opts)), so the changeset's "ctx.api.object(name).count() accepts the two keys" is true, not just typed.
  • DataEngineCountRequestSchema — docs only, right as left. Its query is EngineCountOptionsSchema.extend(RpcLegacyFilterMixin), so it inherits the pair. git grep at this head finds no reader of DataEngineCountRequestSchema / DataEngineRequestSchema outside packages/spec except the generated references (data-engine.mdx, references/index.mdx). No door widens through it today; should a reader appear, what it admits is exactly what engine.count executes, so it cannot become a silent drop. The dev's acceptance note states this correctly.

② Semver level

  • Changeset .changeset/22790-search-total-counted.md: @objectstack/spec minor, @objectstack/objectql minor, @objectstack/metadata-protocol patch; carries Clause-②: yes (widening) and FROM count(object, { where }) → TO count(object, { where, search, searchFields }). Right on every row.
  • spec: two additive optional members on a published accept set plus one added type export → minor, as required by yes. objectql: engine.count (the published IDataEngine.count function schema, zod 540–542) now honours keys it refused as unknown options → an accept-set widening, minor. metadata-protocol: the wire accept set does not move ($search with $top was accepted before); what moves is the value of total from a page-local estimate to the declared meaning of FindDataResponse.total ("Total number of records matching the filter"), and hasMore on an exactly-full last page from true to false → a fix, patch.
  • Not a narrowing anyone could rely on. The response schema is unchanged (total?: number, hasMore: boolean); a client that relied on total == offset + page + 1 relied on the defect against the field's own description, and a client paging until hasMore === false now stops one empty page earlier. The one cost that moves — a COUNT with LIKE arms is issued under a search where none was — is the cost triage accepted, and $count=false stays the opt-out, unchanged.
  • Skew: spec, objectql, metadata-protocol and rest sit in one fixed group in .changeset/config.json, so the four bump together to the group's minor. A metadata-protocol that sends search to the count cannot ship against an objectql that refuses it (that pairing would land in the untouched catch and answer hasMore: false on every page).
  • The PR's Clause-②: yes (widening) agrees with the changeset, the claim (6108682129) and both declarations. Triage's Clause-②: no was superseded by the claim's measured reading and the arm is correct: the count option schema is a published accept set and it grows. Check Changeset and Spec property liveness concluded success on this head.

③ Boundary flags

  • Dev deviation (1) — spec files beyond the first declaration: ruled A (6110016343). The diff holds exactly the ruled set: hook-api.ts (TSDoc only, +4/−3), hook-api.test.ts, type-alias-convention.pin.test.ts, data-engine.test.ts. No further spec source file. Within the ruling.
  • Dev deviation (2) — searchFields beside search: ruled A. The declaration is byte-identical to the feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search #20487 precedent, and the measured reason holds in the code: findData narrows options.searchFields before both calls, so a count given search alone would over-count on stored-metadata tables. Within the ruling.
  • Dev deviation (3) — +301/−62 over 15 files against the 300 suggestion: Check PR Size concluded success. 157 added lines are the real-chain door pin, 25 generated, 17 changeset; source is +61/−42 across four files. Noted, no action.
  • Dev deviation (4) — origin/main not merged in: git merge-tree --write-tree origin/main 4e057d908e with main at 12b9daf7 is clean (exit 0, no conflicts). protocol.ts did move on main since the merge-base — PR fix(metadata-protocol): S5 carry-overs — per-term boot report isolation, one naming of a code-datasource row, seal TSDoc wordings, two changeset rows #22833 (8c105e3d67, merged 2026-10-11T13:52Z), boot-report and TSDoc regions — and merge-tree against fix(metadata-protocol): S5 carry-overs — per-term boot report isolation, one naming of a code-datasource row, seal TSDoc wordings, two changeset rows #22833's head is clean. PR fix(objectql): an aborting after* hook rolls its write back on the default write door #22819 (open draft, a1ed7388, the engine.ts write doors) merge-trees clean against this head too. One stale sentence: the PR body and the dev report say neither neighbour had landed and name main as d8c7d38648, but fix(metadata-protocol): S5 carry-overs — per-term boot report isolation, one naming of a code-datasource row, seal TSDoc wordings, two changeset rows #22833 had merged and main was at 12b9daf7 (2026-10-11T13:48Z) before the PR opened (2026-10-11T14:20Z). The CI pull_request runs were created after both, so their merge ref includes them. A stale sentence, not a conflict.
  • Dev deviation (5) — commit trailers: all four commits carry the AGENTS.md model-free pair (Claude-Session: plus the plain Claude co-author line); the harness reminder's model-named trailer is the one the pre-push hook refuses. Right as done.
  • Dev deviation (6) — the verify lock held 21m54s on the spec repo-tier run: process note, no contract effect.
  • check:dual-build-cjs-loads NOT MEASURED locally (exit 3, prerequisite — 44 packages outside the closure unbuilt). The check-run that answers it is Build Core (ci.yml job build-core, step "Every published require entry point actually loads"), and it concluded success on this head. The dev's narrowed 22-entry probe is a dev-side measurement, not the gate.
  • Check-runs on this head as read: concluded success — Auto Label, Build Core, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate (2/3), Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Temporal Conformance (live PG + MySQL), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, filter; skipped (opt-in) — Console Pin Gate, Packed-tarball smoke; still in progress — Dogfood Regression Gate (1/3) and (3/3), Lint & Repo Gates, Test Core (1/6)–(6/6), Type Check · workspace. Only what concluded is judged here; the queue leg needs every check green regardless of this record, and this record does not wait on them.
  • Governed surfaces: none in the file list (.changeset/, content/docs/references/, packages/**); Governed Surface Queue Guard success.
  • Published skill row understates the set, outside this card: skills/objectstack-query/SKILL.md:27 lists engine count's legal keys as context, where; after this PR the closed set also holds search, searchFields — the same class as feat(spec,objectql,metadata-protocol): grouped and aggregated queries honour search #20487's acceptance note on the aggregate row. Nothing it names is refused. skills/** is Tier H; carrier is the seat, as a docs-only edit on its own PR. Not a defect of this diff.
  • Pre-existing, unchanged: the as any on the count bag in findData (erasure baseline, now carrying only declared keys), and the engine's unknown-option refusal being a bare Error with no ADR-0112 code.

Implemented-by: claude/issue-22790-search-total-count
Reviewed-by: session_01JfJfBUC3cQ6hhgm9MQK76T

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

2 participants